Problem/Motivation
Primitive data types describe their normalized output with the JsonSchema attribute, and Serializer::getJsonSchema() exposes it through the json_schema format. See the change record.
ComplexDataNormalizer does not opt in to the json_schema format. NormalizerBase::checkFormat() rejects it, and the serializer falls back to a placeholder. A map's property definitions already describe the object, and each property can already describe itself. Core should combine them instead of returning a placeholder.
Steps to reproduce
$map = \Drupal::typedDataManager()->create(
MapDataDefinition::create()
->setPropertyDefinition('id', DataDefinition::create('integer')->setRequired(TRUE))
->setPropertyDefinition('label', DataDefinition::create('string'))
);
\Drupal::service('serializer')->getJsonSchema($map, []);
// ['$comment' => 'No schema is defined for property of type Drupal\Core\TypedData\Plugin\DataType\Map. ...']
Expected:
[
'type' => 'object',
'properties' => [
'id' => ['type' => 'integer'],
'label' => ['oneOf' => [['type' => 'string'], ['type' => 'null']]],
],
'required' => ['id'],
]
Proposed resolution
Add SchematicNormalizerHelperTrait to ComplexDataNormalizer and build an object schema from the same non-internal properties that normalize() iterates. SchematicNormalizerTrait does not work here because subclasses such as TimestampItemNormalizer call parent::normalize().
protected function getNormalizationSchema(mixed $object, array $context = []): array {
if (!$object instanceof ComplexDataInterface) {
return ['$comment' => static::generateNoSchemaAvailableMessage($object)];
}
$properties = [];
$required = [];
foreach (TypedDataInternalPropertiesHelper::getNonInternalProperties($object) as $name => $property) {
$properties[$name] = $this->serializer->getJsonSchema($property, $context);
if ($property->getDataDefinition()->isRequired()) {
$required[] = $name;
}
}
return array_filter([
'type' => 'object',
'properties' => $properties,
'required' => $required,
]);
}
JSON:API already does this for field items. \Drupal\jsonapi\Normalizer\FieldItemNormalizer::getNormalizationSchema() builds properties from TypedDataInternalPropertiesHelper::getNonInternalProperties() and adds each property's label as title.
Open questions:
- Should maps set
additionalPropertiestofalse? It matches the definition, butMap::setValue()accepts unknown keys. Leaving it unset matches runtime behavior. EntityNormalizerandFieldItemNormalizerextendComplexDataNormalizerand would inherit the opt-in. Entities do not implementComplexDataInterface, so they get the placeholder from the guard above. Field items would get a schema built from their properties. Should field items keep the placeholder untilFieldItemNormalizerand its subclasses describe their own output?
Remaining tasks
- Decide on
additionalProperties. - Decide whether field items are in scope.
- Merge request with test coverage in
ComplexDataNormalizerTest.
User interface changes
None.
Introduced terminology
None.
API changes
ComplexDataNormalizer supports the json_schema format. Normalization output does not change.
Data model changes
None.
Release notes snippet
AI-Generated: Yes. I used Claude Code to turn my notes into this issue summary and to check the proposed resolution against the Drupal 11.4.6 source. I reviewed and edited the result. The proposed code is a sketch and has not been run.
Comments
Comment #2
mglamanComment #3
mglamanComment #4
mglamanComment #5
mglamanComment #6
mglaman