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.
items but no type: array
a.x has items but no type: array
additionalProperties points back to root
{
"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.
+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.
5. Observable graph growth
The debug graph from --debug print-graph shows repeated # flatten unions sections. The number of nodes climbs instead of converging.
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.