ajv-validator / ajv-validator/ajv
`oneOf` with ~2000+ variants causes "Maximum call stack size exceeded"
- 主要语言
- TypeScript
- 星标
- 14.8k
- 派生
- 1k
- PR 合并指标
- 30 天内没有已合并 PR
描述
## What version of Ajv are you using?
8.17.1
## What problem do you want to solve?
When a JSON Schema contains a `oneOf` with a large number of variants (roughly 2 000+), `ajv.compile()` succeeds but the resulting validator function throws `RangeError: Maximum call stack size exceeded` when called. At even higher counts (~3 000+), `compile()` itself crashes.
This is not a contrived scenario — we maintain a schema for a browser A/B testing platform where feature flags and experiment parameters are described as `oneOf` arrays with 5 000–10 000+ variants generated from a registry.
## The root cause
The `oneOf` codegen (`vocabularies/applicator/oneOf.js`) emits a chain of `if / else { if / else { ... } }` blocks — one nesting level per variant. This produces two problems:
1. **During codegen**: `If.render()` in `compile/codegen/index.js` is recursive — each `If` node calls `this.else.render(opts)`. With N variants the call stack depth is O(N). At ~3 000 variants this exceeds V8's default stack limit and `compile()` throws.
2. **During validation**: Even when codegen succeeds (e.g. N = 2 000), the *generated JavaScript function* itself contains ~2 000 levels of nested `if/else` blocks. When V8 executes this function, it again overflows the call stack.
Setting `code: { optimize: false }` only disables `optimizeNodes()` (which has its own recursion issues) but does not help with `render()` or the generated code structure.
### Relevant code paths
- `compile/codegen/index.js`: `_elseNode()` (line ~616) chains each new `Else` inside the previous `If.else`, creating O(N) nesting depth. `If.render()` (line ~199) recursively calls `this.else.render()`.
- `vocabularies/applicator/oneOf.js`: loop over `schema.oneOf` calling `gen.if(...)` / `.else(...)` for each variant.
## Minimal reproduction
```js
// issue-repro.mjs — run: npm install ajv && node issue-repro.mjs
import Ajv from "ajv";
function makeSchema(n) {
return {
oneOf: Array.from({ length: n }, (_, i) => ({
type: "object",
required: ["name"],
properties: {
name: { const: `variant_${i}` },
},
})),
};
}
for (const n of [100, 500, 1000, 2000, 3000]) {
const ajv = new Ajv({ strict: false, code: { optimize: false } });
const schema = makeSchema(n);
const t0 = performance.now();
try {
const validate = ajv.compile(schema);
const t1 = performance.now();
try {
const result = validate({ name: "variant_0" });
const t2 = performance.now();
console.log(
`oneOf(${n}): compile ${(t1 - t0).toFixed(0)}ms, validate ${(t2 - t1).toFixed(0)}ms, result=${result}`
);
} catch {
console.log(
`oneOf(${n}): compile OK ${(t1 - t0).toFixed(0)}ms, validate() CRASHED — RangeError`
);
}
} catch {
const ms = (performance.now() - t0).toFixed(0);
console.log(`oneOf(${n}): compile() CRASHED after ${ms}ms — RangeError`);
}
}
```
### Expected output
All sizes should compile and validate without errors.
### Actual output (Node.js v22, ajv 8.17.1)
```
oneOf(100): compile 54ms, validate 10ms, result=true
oneOf(500): compile 181ms, validate 203ms, result=true
oneOf(1000): compile 516ms, validate 806ms, result=true
oneOf(2000): compile OK 2565ms, validate() CRASHED — RangeError
oneOf(3000): compile() CRASHED — RangeError
```
## Suggested improvements
### 1. Iterative `If.render()` / `optimizeNodes()`
Replace the recursive `this.else.render()` chain with an iterative loop:
```js
// Instead of:
render(opts) {
let code = `if(${this.condition.render()})` + this.renderBody(opts);
if (this.else) code += "else " + this.else.render(opts); // recursive!
return code;
}
// Could be:
render(opts) {
let code = `if(${this.condition.render()})` + this.renderBody(opts);
let node = this.else;
while (node) {
if (node instanceof If) {
code += `else if(${node.condition.render()})` + node.renderBody(opts);
node = node.else;
} else {
code += "else " + node.renderBody(opts);
node = null;
}
}
return code;
}
```
Output
```
oneOf(100): compile 50ms, validate 10ms, result=true
oneOf(500): compile 181ms, validate 202ms, result=true
oneOf(1000): compile 505ms, validate 786ms, result=true
oneOf(2000): compile OK 2531ms, validate() CRASHED — RangeError
Error compiling schema, function code: .... (long function body)
oneOf(3000): compile() CRASHED after 1503ms — RangeError
```
The same applies to `optimizeNodes()` and `optimizeNames()` on the `If` class.
### 2. Flat code generation for `oneOf`
Instead of nesting each variant inside the previous one's `else` branch, the `oneOf` codegen could emit a flat loop or a series of independent `if` blocks at the same nesting level, since `oneOf` semantics require checking *all* variants anyway (to ensure exactly one matches). This would produce a flat generated function regardless of N.
### 3. HashMap-based dispatch (optimization for `const`/`enum` discriminators)
When every `oneOf` variant is distinguishable by a `const` or `enum` value on a shared property (a very common pattern), AJV could detect this and
贡献指南
评估
这个 Issue 还没有评估数据。