Repository navigation
fix(downgrader): merge the literals of each type in a mixed-type union in 3.1 to 3.0 - #135
Conversation
Zod, ArkType and Valibot write `'a' | 'b' | number` as an `anyOf` with a `const` branch for each literal, and utoipa writes the unit variants of a Rust enum beside its other variants. mergeEnums only merged unions whose enums all share one type, so these stayed an `anyOf` of one-value enums, which NSwag reads as an empty class and oapi-codegen and openapi-python-client as one type per literal. The enums of each type now become one at the first one's place, so scalarOneOf can make a `oneOf` of a union of scalars. A `oneOf` whose branches share a value stays. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016JU51h5pQGFxxMWjNUcAMz
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
mergeEnumsByType— new helper inpackages/downgrader/src/v3.1-to-v3.0.ts:794that groups the eligible enum branches of a mixed-typeanyOf/oneOfbybranch.typeand merges each group's values into one branch at that type's first position, leaving ineligible branches and the union otherwise as they were.mergeEnumsdispatch — whenenumUnionTypeisundefined(the union's non-null branches are not all enums of onetype), it now delegates tomergeEnumsByTypeinstead of returning; theout.type/out.enumguard still runs first, so anenumor a differingtypebeside the union keeps it unchanged.- Tests — a mixed literal union becomes a
oneOfof one enum per type, aoneOfwith a shared value is left intact, and a branch carrying a non-repeating keyword stays separate. - README — one sentence added to the
constrow.
Notes
The merge is an append/union operation, and the oneOf path aborts the whole rewrite when a value repeats within a type group (merging would let that value match a single branch). I confirmed acceptance parity independently: 21,580 generated anyOf/oneOf combinations of mixed const literals, including duplicate values and null branches, produced zero widening and zero loss against a 10-value domain. pnpm test (280 tests) and pnpm run check --only tsc both pass.
One non-blocking observation: a mixed-type union nested inside a oneOf branch that scalarOneOf later flattens is not itself merged (its members are already a oneOf, so the grouping step skips it). That is a missed optimization rather than a regression, and the resulting document is still correct.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

Requested by dinwwwh · project thread
What
Zod, ArkType and Valibot write a union of literals mixed with other types, such as
z.union([z.literal('a'), z.literal('b'), z.number()])or'true' | 'false' | boolean, as ananyOfwith oneconstbranch per literal. utoipa writes the unit variants of a Rust enum beside its other variants the same way.mergeEnumsonly merged unions whose enums all share onetype, so these stayed ananyOfof one-value enums in 3.0. The newmergeEnumsByTypemerges the enums of each type into one at the first one's place, when they hold nothing buttypeandenum(or keywords the schema around them repeats).scalarOneOfthen makes aoneOfof a union of scalars, as it already does forstring | number.A
oneOfwhose branches share a value stays as it is, since merging would let that value through. Anenumor anothertypebeside the union keeps it as before.Why
anyOfas an empty class, and theoneOfas the first branch.anyOfinto empty interfaces, such asexport interface ObjBase {}, and types a query parameter as an empty interface too. With the fix it givesexport type ObjBase = number | stringwith enum value checks. Its Python client gets twooneOfvalidators instead of one per literal; Go is unchanged.oneOfandanyOf.Real documents it changes include GrowthBook and Chroma.
Checks
anyOf/oneOf, nesting and repeated descriptions) changes 5,162 outputs, with 0 values lost or widened. Dropping theoneOfrepeated-value bail-out makes the same fuzz report 64 lost and 1,639 widened, so it does reach that case.pnpm run checkandpnpm testpass (276 tests), with 100% statement, branch, function and line coverage of the downgrader.constrow gets one sentence on the new merge.🤖 Generated with Claude Code
https://claude.ai/code/session_016JU51h5pQGFxxMWjNUcAMz