Skip to content

fix(core): honor --no-combine-classes for structurally identical classes#3039

Open
schani wants to merge 6 commits into
masterfrom
agent/fix-issue-1265
Open

fix(core): honor --no-combine-classes for structurally identical classes#3039
schani wants to merge 6 commits into
masterfrom
agent/fix-issue-1265

Conversation

@schani

@schani schani commented Jul 20, 2026

Copy link
Copy Markdown
Member

The bug

--no-combine-classes had 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-classes and default output were byte-identical — only one Rate class was ever generated for both amount and rate.

Root cause

--no-combine-classes is correctly wired for its documented purpose — disabling the CombineClasses rewrite 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 (via classTypeIdentity), during raw JSON inference (input/Inference.ts's TypeInference.inferClassType) — long before the optional CombineClasses rewrite 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's tryAddToClique already 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: when combineClasses is left on (default), CombineClasses.ts re-merges these classes anyway, so default output is unaffected.

The fix

Thread combineClasses through RunInputDataJSONInputTypeInference, matching the existing inferMaps/inferEnums plumbing. In TypeInference.inferClassType's non-fixed-class branch, when combineClasses is false, tag the class's type attributes with a new TypeAttributeKind whose requiresUniqueIdentity returns true — an existing extension point (already used for StringTypes) that makes TypeBuilder'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), matching combineClasses's own doc comment ("This doesn't apply to classes from a schema, only from inference").

Test coverage

  • Added test/unit/combine-classes.test.ts, calling the quicktype() library API directly with combineClasses: false on JSON equivalent to the issue's sample and asserting both Amount and Rate classes are generated. It also protects the positional fixedTopLevels argument for custom Input implementations.
  • Added the focused csharp-no-combine-classes end-to-end fixture. It runs the existing combined-enum.json sample with the global inference flag disabled, generates separate Foo and Bar classes (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 build passes.
  • Full unit suite: 171/171 tests pass (npx vitest run test/unit/).
  • CLI repro from the issue: --no-combine-classes now generates a separate Amount class in addition to Rate; 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, and FIXTURE=typescript all pass (47/47, 62/62, 79/79) with default settings — no regressions.
  • C# fixtures were checked for generation only; full round-trip execution wasn't available in this environment (dotnet not installed) — CI will cover full C# execution.

Fixes #1265

🤖 Generated with Claude Code

…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>
@github-actions

Copy link
Copy Markdown

No generated-output differences

✅ Generated outputs are unchanged between the PR base and head revisions.

@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown

Generated-output differences

1 files differ — 0 modified, 1 new, 0 deleted
157 changed lines — +157 / −0

Open the generated-output report →

@github-actions

Copy link
Copy Markdown

Generated-output differences

1 files differ — 0 modified, 1 new, 0 deleted
157 changed lines — +157 / −0

Open the generated-output report →

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Is the --no-combine-classes switch broken ?

1 participant