Problem/Motivation

Normalizers that provide JSON Schema disagree on how to describe optional data. See the change record for the JSON Schema support added to the serializer.

  • PrimitiveDataNormalizer::getNormalizationSchema() checks isRequired() and adds a null branch for optional data.
  • DateTimeNormalizer, TimestampNormalizer, and DateTimeIso8601Normalizer use the default getNormalizationSchema() from SchematicNormalizerTrait or SchematicNormalizerHelperTrait. It reflects the JsonSchema attribute and never checks the definition. DateTimeNormalizer::doNormalize() returns NULL when there is no date, so the schema rejects valid output.
  • TypedDataNormalizer::getNormalizationSchema() checks isRequired(), but returns invalid JSON Schema for stringable values. oneOf takes an array of schemas, not type names.
// TypedDataNormalizer::getNormalizationSchema()
return $nullable
  ? ['oneOf' => ['string', 'null']]
  : ['type' => 'string'];

Steps to reproduce

$serializer = \Drupal::service('serializer');
$typed_data_manager = \Drupal::typedDataManager();

$string = $typed_data_manager->create(DataDefinition::create('string'));
$serializer->getJsonSchema($string, []);
// ['oneOf' => [['type' => 'string'], ['type' => 'null']]]

$timestamp = $typed_data_manager->create(DataDefinition::create('timestamp'));
$serializer->getJsonSchema($timestamp, []);
// ['type' => 'string', 'format' => 'date-time']

$serializer->normalize($timestamp);
// NULL

Both definitions are optional. Only the string schema allows null.

Proposed resolution

Decide nullability in one place and pass it to getJsonSchemaForMethod() from both default getNormalizationSchema() implementations. Use the rule PrimitiveDataNormalizer already follows: optional typed data is nullable, and so is a class name, because there is no definition to check.

protected function isNullable(mixed $object): bool {
  return is_string($object)
    || ($object instanceof TypedDataInterface && !$object->getDataDefinition()->isRequired());
}

// SchematicNormalizerTrait
protected function getNormalizationSchema(mixed $object, array $context = []): array {
  return $this->getJsonSchemaForMethod(
    $this,
    'doNormalize',
    ['$comment' => static::generateNoSchemaAvailableMessage($object)],
    $this->isNullable($object),
  );
}

PrimitiveDataNormalizer and TypedDataNormalizer switch to isNullable(). TypedDataNormalizer also fixes its stringable branch:

return $nullable
  ? ['oneOf' => [['type' => 'string'], ['type' => 'null']]]
  : ['type' => 'string'];

Remaining tasks

  • Merge request with test coverage for optional and required data in each normalizer.

User interface changes

None.

Introduced terminology

None.

API changes

JSON Schema for optional dates, timestamps, and stringable typed data allows null. 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.