Skip to content

Commit d8e9b80

Browse files
feat: implement discriminator optimization for JSON schema validation
1 parent e0f39f0 commit d8e9b80

File tree

2 files changed

+869
-1
lines changed

2 files changed

+869
-1
lines changed

src/parser/jsonParser.ts

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

488+
// Try to detect and apply discriminator optimization dynamically
489+
let alternativesToTest = alternatives;
490+
491+
// Attempt to find a common discriminator property across alternatives
492+
const tryDiscriminatorOptimization = () => {
493+
if (alternatives.length < 2) {
494+
return; // No point optimizing with less than 2 alternatives
495+
}
496+
497+
if (node.type === 'object') {
498+
// Early exit: if node has no properties, can't apply optimization
499+
if (!node.properties || node.properties.length === 0) {
500+
return;
501+
}
502+
503+
// Find properties that have const values in ALL alternatives
504+
const propertyConstMap = new Map<string, Map<any, number[]>>();
505+
506+
for (let i = 0; i < alternatives.length; i++) {
507+
const altSchema = asSchema(alternatives[i]);
508+
if (!altSchema.properties) {
509+
// If any alternative has no properties, we can't use object property discrimination
510+
return;
511+
}
512+
for (const propName in altSchema.properties) {
513+
const propSchema = asSchema(altSchema.properties[propName]);
514+
if (propSchema.const !== undefined) {
515+
if (!propertyConstMap.has(propName)) {
516+
propertyConstMap.set(propName, new Map());
517+
}
518+
const constMap = propertyConstMap.get(propName)!;
519+
if (!constMap.has(propSchema.const)) {
520+
constMap.set(propSchema.const, []);
521+
}
522+
constMap.get(propSchema.const)!.push(i);
523+
}
524+
}
525+
}
526+
527+
// Early exit: if no const properties found, can't apply optimization
528+
if (propertyConstMap.size === 0) {
529+
return;
530+
}
531+
532+
// Find a property where ALL alternatives have a const value (one per alternative)
533+
for (const [propName, constMap] of Array.from(propertyConstMap.entries())) {
534+
const coveredAlternatives = new Set<number>();
535+
constMap.forEach((indices) => {
536+
indices.forEach(idx => coveredAlternatives.add(idx));
537+
});
538+
// This property is a valid discriminator ONLY if ALL alternatives have a const value for it
539+
if (coveredAlternatives.size === alternatives.length) {
540+
// Extract the discriminator value from the current node
541+
const prop = node.properties.find(p => p.keyNode.value === propName);
542+
if (prop?.valueNode?.type === 'string') {
543+
const discriminatorValue = prop.valueNode.value;
544+
const matchingIndices = constMap.get(discriminatorValue);
545+
if (matchingIndices && matchingIndices.length > 0) {
546+
alternativesToTest = matchingIndices.map(idx => alternatives[idx]);
547+
return;
548+
}
549+
}
550+
// If this property covers all alternatives but we couldn't match,
551+
break;
552+
}
553+
}
554+
} else if (node.type === 'array') {
555+
// Early exit: if node has no items, can't apply optimization
556+
if (!node.items || node.items.length === 0) {
557+
return;
558+
}
559+
560+
// Find array item indices that have const values in ALL alternatives
561+
const indexConstMap = new Map<number, Map<any, number[]>>();
562+
563+
for (let i = 0; i < alternatives.length; i++) {
564+
const altSchema = asSchema(alternatives[i]);
565+
const itemSchemas = altSchema.prefixItems || (Array.isArray(altSchema.items) ? altSchema.items : undefined);
566+
567+
if (!itemSchemas) {
568+
// If any alternative has no item schemas, we can't use array index discrimination
569+
return;
570+
}
571+
572+
itemSchemas.forEach((itemSchemaRef, itemIndex) => {
573+
const itemSchema = asSchema(itemSchemaRef);
574+
if (itemSchema.const !== undefined) {
575+
if (!indexConstMap.has(itemIndex)) {
576+
indexConstMap.set(itemIndex, new Map());
577+
}
578+
const constMap = indexConstMap.get(itemIndex)!;
579+
if (!constMap.has(itemSchema.const)) {
580+
constMap.set(itemSchema.const, []);
581+
}
582+
constMap.get(itemSchema.const)!.push(i);
583+
}
584+
});
585+
}
586+
587+
// Early exit: if no const items found, can't apply optimization
588+
if (indexConstMap.size === 0) {
589+
return;
590+
}
591+
592+
// Find an index where ALL alternatives have a const value (one per alternative)
593+
for (const [itemIndex, constMap] of Array.from(indexConstMap.entries())) {
594+
const coveredAlternatives = new Set<number>();
595+
constMap.forEach((indices) => {
596+
indices.forEach(idx => coveredAlternatives.add(idx));
597+
});
598+
// This index is a valid discriminator ONLY if ALL alternatives have a const value at this index
599+
if (coveredAlternatives.size === alternatives.length) {
600+
// Extract the discriminator value from the current node
601+
const item = node.items[itemIndex];
602+
if (item?.type === 'string') {
603+
const discriminatorValue = item.value;
604+
const matchingIndices = constMap.get(discriminatorValue);
605+
if (matchingIndices && matchingIndices.length > 0) {
606+
alternativesToTest = matchingIndices.map(idx => alternatives[idx]);
607+
return;
608+
}
609+
}
610+
// If this index covers all alternatives but we couldn't match,
611+
break;
612+
}
613+
}
614+
}
615+
};
616+
617+
tryDiscriminatorOptimization();
618+
488619
// remember the best match that is used for error messages
489620
let bestMatch: { schema: JSONSchema; validationResult: ValidationResult; matchingSchemas: ISchemaCollector; } | undefined = undefined;
490-
for (const subSchemaRef of alternatives) {
621+
for (const subSchemaRef of alternativesToTest) {
491622
const subSchema = asSchema(subSchemaRef);
492623
const subValidationResult = new ValidationResult();
493624
const subMatchingSchemas = matchingSchemas.newSub();

0 commit comments

Comments
 (0)