Skip to content

Commit 63603e5

Browse files
feat: Update discriminator optimization when it has mixed fields
1 parent 7140d70 commit 63603e5

File tree

2 files changed

+142
-10
lines changed

2 files changed

+142
-10
lines changed

src/parser/jsonParser.ts

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -627,14 +627,17 @@ function validate(n: ASTNode | undefined, schema: JSONSchema, validationResult:
627627
return undefined;
628628
}
629629

630-
const buildConstMap = (getSchemas: (alt: JSONSchema, idx: number) => [string | number, JSONSchema][] | undefined) => {
630+
const buildConstMap = (getSchemas: (alt: JSONSchema, idx: number) => [string | number, JSONSchema][] | undefined | null) => {
631631
const constMap = new Map<string | number, Map<any, number[]>>();
632632

633633
for (let i = 0; i < alternatives.length; i++) {
634634
const schemas = getSchemas(asSchema(alternatives[i]), i);
635-
if (!schemas) {
635+
if (schemas === undefined) {
636636
return undefined; // Early exit if any alternative can't be processed
637637
}
638+
if (schemas === null) {
639+
continue; // Skip alternatives that don't have schemas
640+
}
638641

639642
schemas.forEach(([key, schema]) => {
640643
if (schema.const !== undefined) {
@@ -649,29 +652,43 @@ function validate(n: ASTNode | undefined, schema: JSONSchema, validationResult:
649652
}
650653
});
651654
}
652-
return constMap;
655+
656+
// Only return the map if we found const values
657+
return constMap.size > 0 ? constMap : undefined;
653658
};
654659

655660
const findDiscriminator = (constMap: Map<string | number, Map<any, number[]>>, getValue: (key: string | number) => any) => {
661+
// If there are multiple discriminator keys, don't optimize
662+
// This avoids issues with anyOf where different alternatives use different discriminator properties
663+
if (constMap.size > 1) {
664+
return undefined;
665+
}
666+
656667
for (const [key, valueMap] of constMap) {
657668
const coveredAlts = new Set<number>();
658669
valueMap.forEach(indices => indices.forEach(idx => coveredAlts.add(idx)));
659670

660-
if (coveredAlts.size === alternatives.length) {
671+
// Only optimize if discriminator covers at least 2 alternatives and it's worth it
672+
// We don't require ALL alternatives to have discriminators (some might be primitives)
673+
if (coveredAlts.size >= 2) {
661674
const discriminatorValue = getValue(key);
662-
const matchingIndices = valueMap.get(discriminatorValue);
663-
if (matchingIndices?.length) {
664-
return matchingIndices.map(idx => alternatives[idx]);
675+
if (discriminatorValue !== undefined) {
676+
const matchingIndices = valueMap.get(discriminatorValue);
677+
if (matchingIndices?.length) {
678+
return matchingIndices.map(idx => alternatives[idx]);
679+
}
680+
// Discriminator value doesn't match any alternative with const
681+
// Don't optimize - return undefined to test all alternatives
682+
break;
665683
}
666-
break; // Found valid discriminator but no match
667684
}
668685
}
669686
return undefined;
670687
};
671688

672689
if (node.type === 'object' && node.properties?.length) {
673690
const constMap = buildConstMap((schema) =>
674-
schema.properties ? Object.entries(schema.properties).map(([k, v]) => [k, asSchema(v)]) : undefined
691+
schema.properties ? Object.entries(schema.properties).map(([k, v]) => [k, asSchema(v)]) : null
675692
);
676693
if (constMap) {
677694
return findDiscriminator(constMap, (propName) => {
@@ -682,7 +699,7 @@ function validate(n: ASTNode | undefined, schema: JSONSchema, validationResult:
682699
} else if (node.type === 'array' && node.items?.length) {
683700
const constMap = buildConstMap((schema) => {
684701
const itemSchemas = schema.prefixItems || (Array.isArray(schema.items) ? schema.items : undefined);
685-
return itemSchemas ? itemSchemas.map((item, idx) => [idx, asSchema(item)]) : undefined;
702+
return itemSchemas ? itemSchemas.map((item, idx) => [idx, asSchema(item)]) : null;
686703
});
687704
if (constMap) {
688705
return findDiscriminator(constMap, (itemIndex) => {

src/test/parser.test.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3404,4 +3404,119 @@ suite('JSON Parser', () => {
34043404
assert.strictEqual(res.length, 0);
34053405
});
34063406

3407+
test('discriminator optimization with deeply nested self-referencing schema', function () {
3408+
// Schema representing IExpression type with discriminated unions
3409+
// This tests performance with deep nesting (50-100 levels)
3410+
const schema: JSONSchema = {
3411+
$id: 'https://example.com/expression',
3412+
oneOf: [
3413+
{ type: 'string' },
3414+
{ type: 'number' },
3415+
{ type: 'boolean' },
3416+
{ type: 'null' },
3417+
{
3418+
type: 'array',
3419+
prefixItems: [
3420+
{ const: 'logical.and' },
3421+
{ $ref: '#' },
3422+
{ $ref: '#' }
3423+
],
3424+
items: { $ref: '#' },
3425+
minItems: 3
3426+
},
3427+
{
3428+
type: 'array',
3429+
prefixItems: [
3430+
{ const: 'logical.or' },
3431+
{ $ref: '#' },
3432+
{ $ref: '#' }
3433+
],
3434+
items: { $ref: '#' },
3435+
minItems: 3
3436+
},
3437+
{
3438+
type: 'array',
3439+
prefixItems: [
3440+
{ const: 'logical.not' },
3441+
{ $ref: '#' }
3442+
],
3443+
minItems: 2,
3444+
maxItems: 2
3445+
},
3446+
{
3447+
type: 'array',
3448+
prefixItems: [
3449+
{ const: 'compare.eq' },
3450+
{ $ref: '#' },
3451+
{ $ref: '#' }
3452+
],
3453+
minItems: 3,
3454+
maxItems: 3
3455+
}
3456+
]
3457+
};
3458+
{
3459+
// Simple expression
3460+
const { textDoc, jsonDoc } = toDocument('["logical.and", true, false]');
3461+
const semanticErrors = validate2(jsonDoc, textDoc, schema, SchemaDraft.v2020_12);
3462+
assert.strictEqual(semanticErrors!.length, 0);
3463+
}
3464+
{
3465+
// Nested expression (5 levels)
3466+
const { textDoc, jsonDoc } = toDocument('["logical.or", ["logical.and", true, false], ["logical.not", true]]');
3467+
const semanticErrors = validate2(jsonDoc, textDoc, schema, SchemaDraft.v2020_12);
3468+
assert.strictEqual(semanticErrors!.length, 0);
3469+
}
3470+
{
3471+
// Build a deeply nested expression (500 levels)
3472+
let deepExpression = 'true';
3473+
for (let i = 0; i < 500; i++) {
3474+
deepExpression = `["logical.not", ${deepExpression}]`;
3475+
}
3476+
const { textDoc, jsonDoc } = toDocument(deepExpression);
3477+
const semanticErrors = validate2(jsonDoc, textDoc, schema, SchemaDraft.v2020_12);
3478+
assert.strictEqual(semanticErrors!.length, 0);
3479+
}
3480+
{
3481+
// Build an even deeper nested expression (1000 levels)
3482+
let veryDeepExpression = '42';
3483+
for (let i = 0; i < 1000; i++) {
3484+
if (i % 2 === 0) {
3485+
veryDeepExpression = `["logical.not", ${veryDeepExpression}]`;
3486+
} else {
3487+
veryDeepExpression = `["logical.and", ${veryDeepExpression}, false]`;
3488+
}
3489+
}
3490+
const { textDoc, jsonDoc } = toDocument(veryDeepExpression);
3491+
const semanticErrors = validate2(jsonDoc, textDoc, schema, SchemaDraft.v2020_12);
3492+
assert.strictEqual(semanticErrors!.length, 0);
3493+
}
3494+
{
3495+
// Invalid - unknown operator (should fail quickly with discriminator optimization)
3496+
const { textDoc, jsonDoc } = toDocument('["unknown.operator", true, false]');
3497+
const semanticErrors = validate2(jsonDoc, textDoc, schema, SchemaDraft.v2020_12);
3498+
assert.ok(semanticErrors!.length > 0);
3499+
}
3500+
{
3501+
// Invalid - wrong number of arguments for logical.not
3502+
const { textDoc, jsonDoc } = toDocument('["logical.not", true, false]');
3503+
const semanticErrors = validate2(jsonDoc, textDoc, schema, SchemaDraft.v2020_12);
3504+
assert.ok(semanticErrors!.length > 0);
3505+
}
3506+
{
3507+
// Invalid - deeply nested with error (wrong array structure)
3508+
let deepInvalid = '42';
3509+
for (let i = 0; i < 200; i++) {
3510+
deepInvalid = `["logical.not", ${deepInvalid}]`;
3511+
}
3512+
// Invalid: logical.and needs at least 2 arguments after the operator, but we provide wrong structure
3513+
deepInvalid = `["logical.and", ${deepInvalid}]`; // Missing second required argument
3514+
3515+
const { textDoc, jsonDoc } = toDocument(deepInvalid);
3516+
const semanticErrors = validate2(jsonDoc, textDoc, schema, SchemaDraft.v2020_12);
3517+
// Should detect the validation error (not enough items)
3518+
assert.ok(semanticErrors!.length > 0);
3519+
}
3520+
});
3521+
34073522
});

0 commit comments

Comments
 (0)