#2641·ajv

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

Author: allmycodeCreated Jul 1, 2026Updated Jul 1, 2026
Labelsbug report

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

javascript
// 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:

javascript
// 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