Problem/Motivation
The serializer can describe typed data as JSON Schema. See the change record. There is no way back. Given a schema, the only option is to hand-write DataDefinition calls or walk the schema yourself.
Building definitions from a schema turns any published schema into request validation with the Typed Data API, including one generated by another Drupal site:
$definition = $factory->createFromJsonSchema($schema);
$data = \Drupal::typedDataManager()->create($definition, Json::decode($payload));
$violations = $data->validate();
The mapping is mechanical:
| JSON Schema | Typed data |
|---|---|
type: object with properties |
MapDataDefinition with property definitions |
required |
setRequired(TRUE) on each named property |
type: array with items |
ListDataDefinition with setItemDefinition() |
oneOf: [X, {type: null}] |
X, not required |
type: string with format |
email, uri, decimal, datetime_iso8601, duration_iso8601 |
enum, minLength/maxLength, minimum/maximum, pattern |
AllowedValues, Length, Range, Regex |
I have a working userland version that handles a nested object, a list, a nullable property, and each constraint above, then validates a payload against the result: from_json_schema.php.
Steps to reproduce
N/A
Proposed resolution
Add a factory that builds a DataDefinitionInterface from a JSON Schema array. Put it in the serialization module next to the rest of the JSON Schema support, instead of adding a method to TypedDataManagerInterface.
function from_json_schema(array $schema): DataDefinitionInterface {
if (isset($schema['oneOf'])) {
$branches = array_filter($schema['oneOf'], static fn (array $branch) => ($branch['type'] ?? NULL) !== 'null');
if (count($branches) !== 1) {
throw new \InvalidArgumentException('Only nullable oneOf schemas are supported.');
}
return from_json_schema(current($branches));
}
if ($schema['type'] === 'object') {
$definition = MapDataDefinition::create();
foreach ($schema['properties'] ?? [] as $name => $property_schema) {
$property_definition = from_json_schema($property_schema);
$property_definition->setRequired(in_array($name, $schema['required'] ?? [], TRUE));
$definition->setPropertyDefinition($name, $property_definition);
}
return $definition;
}
if ($schema['type'] === 'array') {
return ListDataDefinition::createFromDataType('list')
->setItemDefinition(from_json_schema($schema['items']));
}
$definition = DataDefinition::create(data_type_for_schema($schema['type'], $schema['format'] ?? NULL));
add_constraints_from_schema($schema, $definition);
return $definition;
}
Two gaps prevent an exact round trip, and the factory has to work around both today.
Schemas describe normalized output, not accepted input. timestamp stores an integer, but TimestampNormalizer reports format: date-time. TimestampNormalizer::denormalize() accepts that string, but TypedDataManager::create() sets values directly and skips denormalizers. A definition built from format: date-time has to use datetime_iso8601 instead of timestamp.
Formats have no registry. Mapping a format to a data type is a hardcoded match:
'string' => match ($format) {
'email' => 'email',
'uri' => 'uri',
'number' => 'decimal',
'date-time', 'date' => 'datetime_iso8601',
'duration' => 'duration_iso8601',
default => 'string',
},
Data types could declare the JSON Schema format they accept on the DataType attribute, so contrib data types take part.
Open questions:
- Which keywords are in scope?
$ref,allOf,anyOf, non-nullableoneOf, andadditionalPropertieshave no direct typed data equivalent. Throwing on unsupported keywords is safer than ignoring them, because ignoring them makes validation more permissive than the schema. - JSON Schema
patternuses ECMA-262 syntax without delimiters. TheRegexconstraint needs a PCRE pattern with delimiters, so the factory has to escape the delimiter and add theuflag. This is the reverse of theRegexquestion in #3627330: Validation constraints do not contribute to the JSON Schema. - Should the constraint mapping reuse the interface proposed in #3627330: Validation constraints do not contribute to the JSON Schema, so each constraint knows how to read its own keywords?
Remaining tasks
- Agree on scope and where the factory lives.
- Decide whether the format registry belongs here or in a follow-up.
- Merge request with test coverage.
User interface changes
None.
Introduced terminology
None.
API changes
New service that creates data definitions from JSON Schema. If the format registry is included, the DataType attribute gains a property for the JSON Schema format.
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 comes from a working example that runs against Drupal 11.4.6.
Comments