ajv-validator / ajv-validator/ajv

`oneOf` with ~2000+ variants causes "Maximum call stack size exceeded"

未关闭
#2,641 2 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看
bug report
主要语言
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 还没有评估数据。

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。