Skip to content

fix(core): preserve distinct object alternatives in oneOf/anyOf unions#3042

Open
schani wants to merge 10 commits into
masterfrom
agent/fix-issue-1266
Open

fix(core): preserve distinct object alternatives in oneOf/anyOf unions#3042
schani wants to merge 10 commits into
masterfrom
agent/fix-issue-1266

Conversation

@schani

@schani schani commented Jul 20, 2026

Copy link
Copy Markdown
Member

The bug

Given a JSON Schema oneOf (or anyOf) of two or more disjoint object
schemas, quicktype merged them into a single object type with all properties
made optional (plus an added index signature) instead of generating a
TypeScript union of the distinct object shapes.

Repro (from the issue):

{
  "oneOf": [
    { "type": "object", "required": ["id"], "properties": { "id": { "type": "integer" } } },
    { "type": "object", "required": ["externalId"], "properties": { "externalId": { "type": "string" } } }
  ]
}
quicktype --lang ts --src-lang schema --src AssetIdEither.json --just-types

Before:

export interface AssetIDEither {
    id?: number;
    externalId?: string;
    [property: string]: unknown;
}

After:

export type AssetIDEither = (PurpleAssetIDEither & { "externalId"?: never; }) | (FluffyAssetIDEither & { "id"?: never; });

export interface PurpleAssetIDEither {
    id: number;
    [property: string]: unknown;
}

export interface FluffyAssetIDEither {
    externalId: string;
    [property: string]: unknown;
}

Root cause

Schema parsing built the correct union, but flattenUnions/UnifyUnionBuilder
grouped all object-kind union members together and unconditionally merged
their properties into a single object (UnionType.canBeInlinedInTypeScript
disallowed more than one object-typed member, and UnifyUnionBuilder.makeObject
unioned all their properties as optional). There was no mechanism for marking
some object union members as distinct alternatives that must survive
unification.

The fix

  • Added a new type attribute (packages/quicktype-core/src/attributes/UnionMembers.ts)
    to mark object types that originated as standalone oneOf/anyOf
    alternatives (not allOf/intersection members) as "distinct" union members,
    and to mark oneOf unions as mutually exclusive.
  • JSONSchemaInput.ts now tags standalone oneOf/anyOf object cases with
    this attribute instead of letting them unify like ordinary compatible
    objects.
  • UnionType.canBeInlinedInTypeScript, UnifyUnionBuilder, and
    UnionBuilder were updated to keep multiple distinct-object union members
    separate through flattening/unification instead of collapsing them.
  • Added TargetLanguage.supportsUnionsWithMultipleObjectTypes (default
    false) and enabled it for TypeScript, Flow, JavaScript, and JSON Schema
    output, so other languages are unaffected.
  • For TypeScript specifically, when a union is marked exclusive (from
    oneOf), sibling properties are excluded per-member with "prop"?: never
    guards so the generated union stays exclusive under TypeScript's excess
    property checks.

Test coverage

  • test/inputs/schema/one-of-objects.schema (+ .1.json valid sample and
    .2.fail.one-of.json expected-failure sample exercising the new one-of
    fixture feature) — enabled for the typescript, javascript, and flow
    fixtures via test/languages.ts.
  • test/unit/schema-object-unions.test.ts — a focused unit test reproducing
    the exact issue Union type not supported for objects #1266 schema for both oneOf and anyOf, asserting the
    output is a union of separate interfaces rather than one merged interface.

Coverage was added and confirmed failing against the unmodified code before
implementing the fix, per repo convention.

Verification

  • npm run build — passes.
  • npm run lint (Biome) — zero errors.
  • npx vitest run test/unit — 172/172 tests pass.
  • QUICKTEST=true FIXTURE=typescript,schema-typescript,javascript script/test
    — 208/208 tests pass (run twice to rule out flakiness; an initial run that
    showed a couple of unrelated transient failures under heavy concurrent
    machine load did not reproduce on repeat, or in isolation, or against a
    clean baseline worktree).
  • Manually re-ran the exact repro command from the issue and confirmed the
    corrected union output shown above.
  • Other language fixtures (beyond typescript/javascript/flow, which are the
    only ones opting into this behavior) are left to CI, since
    supportsUnionsWithMultipleObjectTypes defaults to false and this change
    should not affect their generated output.

Fixes #1266

🤖 Generated with Claude Code

schani and others added 7 commits July 20, 2026 18:24
#1266)

Co-Authored-By: gpt-5.6-sol via pi <noreply@openai.com>
The new one-of-objects.schema fixture uses a top-level array, which the
PHP driver does not support (generated TopLevel::from expects stdClass,
not array) - the same pre-existing limitation for which union.schema is
already skipped. The oneOf-of-objects behavior this fixture exercises is
only enabled for TS/JS/Flow, so PHP output is unaffected.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
…T> (#1266)

emitTopLevelArray unconditionally emitted `typealias X = JsonArray<T>`,
but kotlinx.serialization.json.JsonArray is a concrete, non-generic
class, not a generic container. This only compiled by accident for
element types that stringify to JsonObject/JsonElement (the one case
where a bare `JsonArray` is correct). The new one-of-objects.schema
fixture (top-level array of a oneOf union of distinct objects) exposed
this for the first time, breaking the kotlinx/schema-kotlinx CI job.

Mirror the same JsonObject/JsonElement special case already used in
arrayType(): emit bare `JsonArray` for raw JSON element arrays, and a
real generic `List<T>` otherwise.

Co-Authored-By: gpt-5.6-sol via pi <noreply@openai.com>
…st (#1266)

test/unit/cjson-enum-default.test.ts (landed on master in 89d9f2a)
hardcoded its expected enum body in alphabetical order, but master's
ConvenienceRenderer.forEachEnumCase (f58485e, "preserve JSON Schema
enum case order") already stopped alphabetizing enum cases in favor of
source declaration order. The test was never updated to match, so it
fails identically on master alone (verified independent of this PR's
changes) and blocks PR #1266's CI once merged with base. Correct the
expected case order to the declaration order (state, config,
heartbeat) that quicktype now intentionally produces; the test's
actual purpose (asserting cJSON enums start at 1) is unaffected.

Co-Authored-By: gpt-5.6-sol via pi <noreply@openai.com>
@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown

Generated-output differences

57 files differ — 22 modified, 35 new, 0 deleted
14619 changed lines — +10259 / −4360

Open the generated-output report →

@github-actions

Copy link
Copy Markdown

Generated-output differences

58 files differ — 22 modified, 36 new, 0 deleted
14695 changed lines — +10335 / −4360

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.

Union type not supported for objects

1 participant