quicktype issue #2187 — minimized crash trigger

A tiny schema that makes type flattening unroll forever

The schema is valid JSON Schema, but mostly unconstrained. quicktype expands omitted type fields into broad unions, then repeatedly tries to flatten recursive unions. Each pass creates one more layer instead of closing the recursive loop.

Root has items but no type: array a.x has items but no type: array additionalProperties points back to root
/tmp/bug.min.schema
{
  "items": {
    "anyOf": [
      { "$ref": "#/definitions/a" },
      { "additionalProperties": { "$ref": "#" } }
    ]
  },
  "definitions": {
    "a": {
      "properties": {
        "x": { "items": { "$ref": "#/definitions/a" } }
      }
    }
  }
}

1. What quicktype initially sees

Because JSON Schema keywords such as items, properties, and additionalProperties do not imply a type by themselves, quicktype must keep all JSON kinds alive: scalars, objects, arrays, etc.

Root₀ Scalar | AnyObj | array<Item₀> from root.items.anyOf Item₀ A₀ | M₀ the anyOf branches A₀ Scalar | AnyArr | { x?: X₀, +any } definition a M₀ Scalar | AnyArr | { +Root₀ } additionalProperties: # X₀ Scalar | AnyObj | array<A₀> x points to X₀ M₀ points back to Root₀
union recursive reference +T means additionalProperties of type T

2. The first flattening pass: merge the two anyOf branches

Before

Item₀ = A₀ | M₀

A₀ object branch =
  { x?: X₀, additionalProperties: any }

M₀ object branch =
  { additionalProperties: Root₀ }

After object merge

Item₁ = Scalar | AnyArr |
  {
    x?: X₀ | Root₀,
    additionalProperties: any | Root₀
  }

Why does Root₀ get added to x? Because the second object has no explicit x, but it allows arbitrary properties whose values are Root₀. Therefore its potential x value is Root₀.

3. Step through the unroll

Pass 1 creates Item₁

Item₁ = Scalar | AnyArr |
  {
    x?: X₀ | Root₀,
    additionalProperties: any | Root₀
  }

The two object branches are merged. The missing property x in the map-like branch is filled from its additionalProperties type, which is Root₀.

4. The visual shape: finite chains instead of a recursive knot

Correctly, this should close into one recursive type. Instead, each flatten pass allocates a fresh deeper chain.

What quicktype keeps building: Item₁object { x } X₁array<Item₀> Item₀older layer Item₂object { x } X₂array<Item₁> Item₁previous layer Item₃object { x } X₃array<Item₂> Item₂previous layer What should happen: Item*Scalar | AnyArr |object { x?: X* } X*array<Item*>

5. Observable graph growth

The debug graph from --debug print-graph shows repeated # flatten unions sections. The number of nodes climbs instead of converging.

24resolve
24flat 1
26flat 4
62flat 19
~100slater
~2800crash

The core bug

quicktype is building deeper finite approximations:

Item₁ -> x -> array<Item₀>
Item₂ -> x -> array<Item₁>
Item₃ -> x -> array<Item₂>
...

It should instead recognize the recursive fixed point:

Item* -> x -> array<Item*>

Fix direction

The robust fix is to make union flattening cycle-aware: allocate forwarding references for the recursive component first, then rewrite children to those refs. A narrower mitigation is to simplify broad unions equivalent to any, but that is less general.