Commit d7e45f9
authored
fix(json-schema): keep sibling union branches that share a $defs entry (#1927)
Sibling `anyOf`/`oneOf` branches that reference the same `$defs` entry
no longer collapse into one when a union is flattened.
`flattenJsonUnionSchemaInternal` tracked in-progress `$ref`s in a Set it
mutated in place, and since `Set.prototype.add` returns the same set, a
`$ref` being resolved leaked out of its own recursion path and persisted
into later sibling branches. Any branch pointing at an entry an earlier
sibling had already touched hit the cycle guard and was silently
dropped.
Copying the set per recursion path scopes the guard to the path actually
being walked, matching what the sibling function
`extractJsonObjectSchemaEntriesInternal` already does.
## Fixes
The reported case now keeps both branches with their own keywords
instead of returning only the first:
```ts
flattenJsonUnionSchema({
$defs: { Shared: { type: 'string' } },
anyOf: [
{ $ref: '#/$defs/Shared', minLength: 2 },
{ $ref: '#/$defs/Shared', maxLength: 5 },
],
})
```
Downstream, OpenAPI documents generated from unions whose members share
a `$ref` no longer lose members: detailed output schemas keep every
response status, and request/response bodies keep every variant. Cycle
detection stays intact and still terminates, including on mutually
recursive `$defs`.
## Performance
The in-place mutation was acting as accidental global memoization, so
flattening is now exponential in union nesting depth in the worst case.
This only bites on shapes far outside normal contracts, because
flattening recurses through `anyOf`/`oneOf` and top-level `$ref` only,
never into `properties` or `items`. A union nested 7 deep with 3
branches per level measures ~20 ms; a synthetic depth-20 chain of
unions-of-unions takes seconds. Generation happens at build/startup on
developer-authored schemas, and the previous cost was paid in silently
wrong output, so the trade seemed worth making rather than keeping a
faster incorrect traversal.
Worth noting for anyone who revisits this: a memo would have to be keyed
on the resolved definition *plus* the path-relevant ref set. Keying on
schema identity alone would collapse legitimately distinct branches and
reintroduce exactly this bug.
## Testing
Three regression tests, each confirmed to fail before the change and
pass after: the reported repro, a shared `$defs` entry that is itself a
union (2 branches to 4), and mutual `A` to `B` recursion, which also
pins that cycle detection stays path-scoped and terminating.
Full suite (3251 tests), `pnpm type:check` and `pnpm lint` pass.
Closes #19261 parent 0c7ec80 commit d7e45f9
2 files changed
Lines changed: 50 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
839 | 839 | | |
840 | 840 | | |
841 | 841 | | |
| 842 | + | |
| 843 | + | |
| 844 | + | |
| 845 | + | |
| 846 | + | |
| 847 | + | |
| 848 | + | |
| 849 | + | |
| 850 | + | |
| 851 | + | |
| 852 | + | |
| 853 | + | |
| 854 | + | |
| 855 | + | |
| 856 | + | |
| 857 | + | |
| 858 | + | |
| 859 | + | |
| 860 | + | |
| 861 | + | |
| 862 | + | |
| 863 | + | |
| 864 | + | |
| 865 | + | |
| 866 | + | |
| 867 | + | |
| 868 | + | |
| 869 | + | |
| 870 | + | |
| 871 | + | |
| 872 | + | |
| 873 | + | |
842 | 874 | | |
843 | 875 | | |
844 | 876 | | |
| |||
882 | 914 | | |
883 | 915 | | |
884 | 916 | | |
| 917 | + | |
| 918 | + | |
| 919 | + | |
| 920 | + | |
| 921 | + | |
| 922 | + | |
| 923 | + | |
| 924 | + | |
| 925 | + | |
| 926 | + | |
| 927 | + | |
| 928 | + | |
| 929 | + | |
| 930 | + | |
| 931 | + | |
| 932 | + | |
| 933 | + | |
885 | 934 | | |
886 | 935 | | |
887 | 936 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
380 | 380 | | |
381 | 381 | | |
382 | 382 | | |
383 | | - | |
| 383 | + | |
384 | 384 | | |
385 | 385 | | |
386 | 386 | | |
| |||
0 commit comments