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 additionalProperties to false? It matches the definition, but Map::setValue() accepts unknown keys. Leaving it unset matches runtime behavior.
  • EntityNormalizer and FieldItemNormalizer extend ComplexDataNormalizer and would inherit the opt-in. Entities do not implement ComplexDataInterface, 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 until FieldItemNormalizer and 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

mglaman created an issue. See original summary.

mglaman’s picture

Issue summary: View changes
mglaman’s picture

Issue summary: View changes
mglaman’s picture

Issue summary: View changes
mglaman’s picture

Issue summary: View changes
mglaman’s picture

Issue tags: +rosetta