Skip to content

Commit 2fb9a34

Browse files
committed
fixed exponential AJV validation time by stripping recursive schema properties before calling isValid
1 parent a1cee75 commit 2fb9a34

2 files changed

Lines changed: 35 additions & 3 deletions

File tree

packages/utils/src/schema/getFirstMatchingOption.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,37 @@
11
import get from 'lodash/get';
22
import has from 'lodash/has';
33
import isNumber from 'lodash/isNumber';
4+
import isObject from 'lodash/isObject';
45

5-
import { PROPERTIES_KEY } from '../constants';
6+
import { ANY_OF_KEY, ALL_OF_KEY, ONE_OF_KEY, PROPERTIES_KEY, REF_KEY } from '../constants';
67
import getOptionMatchingSimpleDiscriminator from '../getOptionMatchingSimpleDiscriminator';
78
import { FormContextType, RJSFSchema, StrictRJSFSchema, ValidatorType } from '../types';
89

10+
/** Returns a copy of `schema` with properties that could cause deep recursive AJV
11+
* validation (oneOf, anyOf, allOf, $ref) replaced by `{}` (accept any value).
12+
* This prevents O(options^depth) validation work in schemas with cross-referencing definitions.
13+
*/
14+
function stripRecursiveProperties<S extends StrictRJSFSchema = RJSFSchema>(schema: S): S {
15+
const properties = schema[PROPERTIES_KEY];
16+
if (!isObject(properties)) {
17+
return schema;
18+
}
19+
const shallow: Record<string, unknown> = {};
20+
for (const [key, prop] of Object.entries(properties)) {
21+
if (typeof prop !== 'object' || prop === null) {
22+
shallow[key] = prop;
23+
} else if (ONE_OF_KEY in prop || ANY_OF_KEY in prop || ALL_OF_KEY in prop || REF_KEY in prop) {
24+
shallow[key] = {};
25+
} else {
26+
shallow[key] = prop;
27+
}
28+
}
29+
return { ...schema, [PROPERTIES_KEY]: shallow };
30+
}
31+
932
/** Given the `formData` and list of `options`, attempts to find the index of the first option that matches the data.
33+
* Matching is shallow: properties containing `oneOf`, `anyOf`, `allOf`, or `$ref` are treated as unconstrained during
34+
* validation to avoid exponential AJV validation time with cross-referencing schemas.
1035
* Always returns the first option if there is nothing that matches.
1136
*
1237
* @param validator - An implementation of the `ValidatorType` interface that will be used when necessary
@@ -91,10 +116,10 @@ export default function getFirstMatchingOption<
91116
// been filled in yet, which will mean that the schema is not valid
92117
delete augmentedSchema.required;
93118

94-
if (validator.isValid(augmentedSchema, formData, rootSchema)) {
119+
if (validator.isValid(stripRecursiveProperties(augmentedSchema as S), formData, rootSchema)) {
95120
return i;
96121
}
97-
} else if (validator.isValid(option, formData, rootSchema)) {
122+
} else if (validator.isValid(stripRecursiveProperties(option), formData, rootSchema)) {
98123
return i;
99124
}
100125
}

packages/utils/test/schema/getFirstMatchingOptionTest.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,5 +212,12 @@ export default function getFirstMatchingOptionTest(testValidator: TestValidatorT
212212
}
213213
consoleWarnSpy.mockRestore();
214214
});
215+
it('handles options with boolean property schemas', () => {
216+
testValidator.setReturnValues({ isValid: [true] });
217+
const options: RJSFSchema[] = [
218+
{ properties: { foo: true, bar: { type: 'string' } } },
219+
];
220+
expect(getFirstMatchingOption(testValidator, { bar: 'baz' }, options, rootSchema)).toEqual(0);
221+
});
215222
});
216223
}

0 commit comments

Comments
 (0)