Problem/Motivation
The JsonSchema attribute describes the type of a normalized value. Validation constraints on the data definition never reach the schema. See the change record for the JSON Schema support added to the serializer.
A consumer that validates against the schema accepts values Drupal rejects. A currency code limited to USD and CAD is published as any string.
Several core constraints map directly onto JSON Schema validation keywords:
| Constraint | Keywords |
|---|---|
AllowedValues |
enum |
Length |
minLength, maxLength |
Range |
minimum, maximum |
Regex |
pattern |
Count |
minItems, maxItems |
Email |
format: email |
Steps to reproduce
$definition = DataDefinition::create('string')
->setRequired(TRUE)
->addConstraint('AllowedValues', ['USD', 'CAD'])
->addConstraint('Length', ['max' => 3]);
$data = \Drupal::typedDataManager()->create($definition);
\Drupal::service('serializer')->getJsonSchema($data, []);
// ['type' => 'string']
Expected:
['type' => 'string', 'enum' => ['USD', 'CAD'], 'maxLength' => 3]
Proposed resolution
Add an interface that constraints implement to describe themselves as JSON Schema keywords. Each constraint in the table above already has a core subclass (AllowedValuesConstraint, LengthConstraint, and so on), so core can implement it without touching Symfony's classes. Contrib constraints opt in the same way.
interface JsonSchemaConstraintInterface {
/**
* Gets the JSON Schema keywords that express this constraint.
*
* @return array<string, mixed>
* JSON Schema validation keywords.
*/
public function getJsonSchemaKeywords(): array;
}
class LengthConstraint extends Length implements JsonSchemaConstraintInterface {
public function getJsonSchemaKeywords(): array {
return array_filter([
'minLength' => $this->min,
'maxLength' => $this->max,
], is_int(...));
}
}
SchematicNormalizerHelperTrait merges the keywords into the schema. Constraints come from TypedDataInterface::getConstraints(), which includes the ones the data type adds. When the value is optional, the schema is a oneOf with a null branch, and the keywords only belong on the non-null branch.
protected function addConstraintKeywords(array $schema, TypedDataInterface $data): array {
$keywords = [];
foreach ($data->getConstraints() as $constraint) {
if ($constraint instanceof JsonSchemaConstraintInterface) {
$keywords += $constraint->getJsonSchemaKeywords();
}
}
if (isset($schema['oneOf'])) {
$schema['oneOf'][0] += $keywords;
return $schema;
}
return $schema + $keywords;
}
Open questions:
- Some constraint options cannot be expressed in JSON Schema.
RangesupportsminPropertyPathandmaxPropertyPath.Regexsupportsmatch: FALSE, which needsnot. Should these return no keywords, or a partial schema? Regexstores a PCRE pattern with delimiters and flags. JSON Schemapatternuses ECMA-262 syntax without delimiters. Symfony'sRegex::getHtmlPattern()already converts to ECMA-262, but anchors the pattern the way HTML does. Is it close enough to reuse?Choiceis registered as Symfony's class directly, so it cannot implement the interface. IsAllowedValuesenough?Countapplies to lists, so the keywords also need to reachListNormalizer. See #3627328: ListNormalizer does not provide a JSON Schema for lists.
Remaining tasks
- Agree on the interface name and namespace.
- Implement the interface on the core constraints in the table.
- Merge request with test coverage.
User interface changes
None.
Introduced terminology
None.
API changes
New JsonSchemaConstraintInterface. JSON Schema output for constrained data gains validation keywords. 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
mglaman