Skip to content

Commit 1e06466

Browse files
feat: add discriminator support for JSON schema validation (#292)
* feat: implement discriminator optimization for JSON schema validation * polish and extract common patterns --------- Co-authored-by: Martin Aeschlimann <[email protected]>
1 parent e0920ac commit 1e06466

File tree

2 files changed

+810
-1
lines changed

2 files changed

+810
-1
lines changed

src/parser/jsonParser.ts

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -485,9 +485,11 @@ function validate(n: ASTNode | undefined, schema: JSONSchema, validationResult:
485485
const testAlternatives = (alternatives: JSONSchemaRef[], maxOneMatch: boolean) => {
486486
const matches = [];
487487

488+
const alternativesToTest = _tryDiscriminatorOptimization(alternatives) ?? alternatives;
489+
488490
// remember the best match that is used for error messages
489491
let bestMatch: { schema: JSONSchema; validationResult: ValidationResult; matchingSchemas: ISchemaCollector; } | undefined = undefined;
490-
for (const subSchemaRef of alternatives) {
492+
for (const subSchemaRef of alternativesToTest) {
491493
const subSchema = asSchema(subSchemaRef);
492494
const subValidationResult = new ValidationResult();
493495
const subMatchingSchemas = matchingSchemas.newSub();
@@ -620,7 +622,77 @@ function validate(n: ASTNode | undefined, schema: JSONSchema, validationResult:
620622
}
621623
}
622624

625+
function _tryDiscriminatorOptimization(alternatives: JSONSchemaRef[]): JSONSchemaRef[] | undefined {
626+
if (alternatives.length < 2) {
627+
return undefined;
628+
}
629+
630+
const buildConstMap = (getSchemas: (alt: JSONSchema, idx: number) => [string | number, JSONSchema][] | undefined) => {
631+
const constMap = new Map<string | number, Map<any, number[]>>();
632+
633+
for (let i = 0; i < alternatives.length; i++) {
634+
const schemas = getSchemas(asSchema(alternatives[i]), i);
635+
if (!schemas) {
636+
return undefined; // Early exit if any alternative can't be processed
637+
}
638+
639+
schemas.forEach(([key, schema]) => {
640+
if (schema.const !== undefined) {
641+
if (!constMap.has(key)) {
642+
constMap.set(key, new Map());
643+
}
644+
const valueMap = constMap.get(key)!;
645+
if (!valueMap.has(schema.const)) {
646+
valueMap.set(schema.const, []);
647+
}
648+
valueMap.get(schema.const)!.push(i);
649+
}
650+
});
651+
}
652+
return constMap;
653+
};
654+
655+
const findDiscriminator = (constMap: Map<string | number, Map<any, number[]>>, getValue: (key: string | number) => any) => {
656+
for (const [key, valueMap] of constMap) {
657+
const coveredAlts = new Set<number>();
658+
valueMap.forEach(indices => indices.forEach(idx => coveredAlts.add(idx)));
623659

660+
if (coveredAlts.size === alternatives.length) {
661+
const discriminatorValue = getValue(key);
662+
const matchingIndices = valueMap.get(discriminatorValue);
663+
if (matchingIndices?.length) {
664+
return matchingIndices.map(idx => alternatives[idx]);
665+
}
666+
break; // Found valid discriminator but no match
667+
}
668+
}
669+
return undefined;
670+
};
671+
672+
if (node.type === 'object' && node.properties?.length) {
673+
const constMap = buildConstMap((schema) =>
674+
schema.properties ? Object.entries(schema.properties).map(([k, v]) => [k, asSchema(v)]) : undefined
675+
);
676+
if (constMap) {
677+
return findDiscriminator(constMap, (propName) => {
678+
const prop = node.properties.find(p => p.keyNode.value === propName);
679+
return prop?.valueNode?.type === 'string' ? prop.valueNode.value : undefined;
680+
});
681+
}
682+
} else if (node.type === 'array' && node.items?.length) {
683+
const constMap = buildConstMap((schema) => {
684+
const itemSchemas = schema.prefixItems || (Array.isArray(schema.items) ? schema.items : undefined);
685+
return itemSchemas ? itemSchemas.map((item, idx) => [idx, asSchema(item)]) : undefined;
686+
});
687+
if (constMap) {
688+
return findDiscriminator(constMap, (itemIndex) => {
689+
const item = node.items[itemIndex as number];
690+
return item?.type === 'string' ? item.value : undefined;
691+
});
692+
}
693+
}
694+
return undefined;
695+
}
624696

625697
function _validateNumberNode(node: NumberASTNode): void {
626698
const val = node.value;

0 commit comments

Comments
 (0)