Skip to content

fix(schema): respect "required" for properties inside oneOf/anyOf#3041

Open
schani wants to merge 4 commits into
masterfrom
agent/fix-issue-1104
Open

fix(schema): respect "required" for properties inside oneOf/anyOf#3041
schani wants to merge 4 commits into
masterfrom
agent/fix-issue-1104

Conversation

@schani

@schani schani commented Jul 20, 2026

Copy link
Copy Markdown
Member

Bug

required had no effect on properties reached through JSON Schema oneOf/anyOf. A property required in the branch where it's declared would come out optional in generated code, while the exact same property/required shape outside of oneOf/anyOf correctly stayed required.

Repro (from the issue, --src-lang schema --lang typescript):

{
  "properties": {
    "anyof": {
      "anyOf": [
        { "type": "object", "properties": { "name": {"type": ["string","null"]} }, "required": ["name"], "additionalProperties": false },
        { "type": "object", "properties": { "size": {"type": "string"} }, "required": [], "additionalProperties": false }
      ]
    }
  }
}

Before:

export interface Anyof {
    name?: null | string;   // wrong: required in its branch, still comes out optional
    size?: string;
}

After:

export interface Anyof {
    name: null | string;    // correct
    size?: string;
}

Root cause

quicktype's default pipeline flattens a union of several object (ClassType) branches — which is how oneOf/anyOf alternatives are represented internally — into a single merged interface (FlattenUnions.tsUnifyClasses.ts). The property-merging logic (getCliqueProperties in UnifyClasses.ts) unconditionally marked a property optional whenever it was absent from any one of the merged branches, without considering that the branch it's missing from might be a closed schema (additionalProperties: false) that simply doesn't declare it — so the presence/absence heuristic (correct for merging differently-shaped plain JSON samples) silently dropped the required information declared for JSON Schema oneOf/anyOf alternatives.

Fix

  • Added a schemaSetOperationTypeAttributeKind type attribute (packages/quicktype-core/src/attributes/Schema.ts) that JSONSchemaInput.ts stamps on the union type it builds for oneOf/anyOf (convertOneOrAnyOf).
  • FlattenUnions.ts checks for that attribute on the unions it's about to merge and, when present, tells UnifyUnionBuilder/getCliqueProperties (UnifyClasses.ts) to preserve a property's required-ness instead of forcing it optional purely because a closed sibling branch (additionalProperties: false) doesn't declare it. Properties still become optional when the branch that lacks them is "open" (could carry the property as untyped additional data) or when the property is itself optional within the branch(es) that do declare it.
  • Plain multi-sample JSON inference (no JSON Schema involved) is untouched — presence-based optionality there is still correct and desired, since it's not gated by the new attribute.

Test coverage

Added test/inputs/schema/required-in-any-of.schema (a two-branch anyOf, one branch requiring name, the other requiring nothing and being closed) plus .1.json (valid) and .2.fail.strict-optional.json (an object missing name, expected to fail under the strict-optional feature — TypeScript's schema fixture already declares that feature). This is picked up automatically by the existing schema-typescript fixture.

Verification

  • New fixture (required-in-any-of.schema) fails on unfixed code, passes after the fix.
  • QUICKTEST=true FIXTURE=schema-typescript script/test — all 71 cases pass.
  • npx vitest run test/unit — 170 tests pass.
  • npm run build passes.
  • Manually confirmed the exact repro from the issue now generates name: null | string; instead of name?: null | string;.
  • Manually confirmed plain (non-schema) multi-sample JSON inference still generates optional properties for fields missing in some samples (unaffected).
  • CI will additionally validate the other language schema fixtures.

Fixes #1104

🤖 Generated with Claude Code

schani and others added 4 commits July 20, 2026 18:20
)

Co-Authored-By: gpt-5.6-sol via pi <noreply@openai.com>
The oneOf/anyOf `required` fix (#1104) can make a self-referential
property required, e.g. FacetSpec/RepeatSpec in vega-lite require a
nested `spec`. Previously such members were optional and the C++
renderer stored them as `std::shared_ptr` (heap indirection) via the
`isCycleBreakerType` path in `isOptionalAsValuePossible`, which broke
the cycle. A required member skipped that path and was emitted by value
(`Spec spec;`), producing an incomplete recursive type that fails to
compile.

Mirror the optional behaviour: a required class-typed member that is a
cycle-breaker type now also gets heap indirection, so recursive required
members compile. Verified all 71 schema-cplusplus fixtures pass,
including vega-lite.

Also scope out the new `required-in-any-of.schema` for Elixir, which —
like the existing `strict-optional.schema`/`required.schema` — cannot
enforce required struct keys at runtime, so its `.fail.strict-optional`
sample does not fail as the fixture expects.

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Generated-output differences

154 files differ — 77 modified, 53 new, 24 deleted
14504 changed lines — +8094 / −6410

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.

"required": [ ... ] does not have any effect when inside oneOf/anyOf

2 participants