fix(core): honor --no-combine-classes for structurally identical classes#3039
Open
schani wants to merge 6 commits into
Open
fix(core): honor --no-combine-classes for structurally identical classes#3039schani wants to merge 6 commits into
schani wants to merge 6 commits into
Conversation
…ses (#1265) Structurally identical objects inferred from JSON at different paths (e.g. `amount: { initialValue }` and `rate: { initialValue }`) were deduplicated into a single class by TypeBuilder's identity-based type cache during raw JSON inference, before the optional combineClasses rewrite pass ever ran. Since --no-combine-classes only disables that later rewrite pass, it had no effect on this case, even though it correctly controlled merging of classes with overlapping-but-not-identical properties. Thread combineClasses through Run -> InputData -> JSONInput -> TypeInference, and when it's disabled, mark freshly inferred non-fixed class types with a type attribute that forces unique identity, so they're no longer collapsed by TypeBuilder's structural cache or later graph rewrites. When combineClasses is enabled (the default), CombineClasses.ts's existing unconditional "structurally compatible" merge re-unifies these classes, so default output is unaffected. Co-Authored-By: gpt-5.6-sol via pi <noreply@openai.com>
No generated-output differences✅ Generated outputs are unchanged between the PR base and head revisions. |
Generated-output differences1 files differ — 0 modified, 1 new, 0 deleted |
# Conflicts: # test/fixtures.ts # test/languages.ts
Generated-output differences1 files differ — 0 modified, 1 new, 0 deleted |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
--no-combine-classeshad no effect for a specific case reported in #1265: when two JSON objects at different paths in the input have exactly the same shape (same property names and types), quicktype always merges them into one class, regardless of the flag.Example: given
{ "amount": { "initialValue": 1000000.0 }, "rate": { "initialValue": 0.012 } }--no-combine-classesand default output were byte-identical — only oneRateclass was ever generated for bothamountandrate.Root cause
--no-combine-classesis correctly wired for its documented purpose — disabling theCombineClassesrewrite pass, which merges classes with overlapping-but-not-identical properties (verified this works correctly with a non-identical-overlap test case; it toggles between merged/separate as expected).The reported case is different: classes that are exactly, structurally identical. Those get deduplicated much earlier, in
TypeBuilder.getClassType(viaclassTypeIdentity), during raw JSON inference (input/Inference.ts'sTypeInference.inferClassType) — long before the optionalCombineClassesrewrite pass, gated by the flag, ever runs. So there was no code path left for the flag to affect in this case.Note
CombineClasses.ts'stryAddToCliquealready has, as an unconditional first step, a check that merges exact structural duplicates — this is what makes it safe to stop deduplicating at the identity-cache level for this case: whencombineClassesis left on (default),CombineClasses.tsre-merges these classes anyway, so default output is unaffected.The fix
Thread
combineClassesthroughRun→InputData→JSONInput→TypeInference, matching the existinginferMaps/inferEnumsplumbing. InTypeInference.inferClassType's non-fixed-class branch, whencombineClassesisfalse, tag the class's type attributes with a newTypeAttributeKindwhoserequiresUniqueIdentityreturnstrue— an existing extension point (already used forStringTypes) that makesTypeBuilder's identity-based type cache skip deduplication for that type, so it survives as a distinct class through inference and later graph rewrites instead of being silently unified with same-shaped classes from other paths.This only affects classes built from JSON inference (
getClassType), not JSON-Schema-derived classes (getUniqueClassType/isFixed: true), matchingcombineClasses's own doc comment ("This doesn't apply to classes from a schema, only from inference").Test coverage
test/unit/combine-classes.test.ts, calling thequicktype()library API directly withcombineClasses: falseon JSON equivalent to the issue's sample and asserting bothAmountandRateclasses are generated. It also protects the positionalfixedTopLevelsargument for customInputimplementations.csharp-no-combine-classesend-to-end fixture. It runs the existingcombined-enum.jsonsample with the global inference flag disabled, generates separateFooandBarclasses (and their separate enums), and participates in the normal C# compile/round-trip fixture group. This also makes the option's generated output visible to the output-difference workflow.Verification
npm run buildpasses.npx vitest run test/unit/).--no-combine-classesnow generates a separateAmountclass in addition toRate; default output is confirmed byte-identical (md5-matched) to before the fix, for both the issue's sample and a separate near-duplicate-overlap sample used to confirm the pre-existing "similar classes" merging behavior is untouched.QUICKTEST=true FIXTURE=golang script/test,FIXTURE=python, andFIXTURE=typescriptall pass (47/47, 62/62, 79/79) with default settings — no regressions.dotnetnot installed) — CI will cover full C# execution.Fixes #1265
🤖 Generated with Claude Code