Skip to content

fix: preserve multi-type type union when schema has subschemas (#954)#1001

Open
ndreno wants to merge 2 commits into
oxidecomputer:mainfrom
barbacane-dev:fix/issue-954-type-union-with-subschemas-clean
Open

fix: preserve multi-type type union when schema has subschemas (#954)#1001
ndreno wants to merge 2 commits into
oxidecomputer:mainfrom
barbacane-dev:fix/issue-954-type-union-with-subschemas-clean

Conversation

@ndreno
Copy link
Copy Markdown

@ndreno ndreno commented Apr 24, 2026

Re-submission of #1000 rebased directly on main. The earlier branch sat on top of other unrelated fork changes that altered doc-comment JSON field ordering, so the committed .rs fixture didn't match upstream codegen and cargo test failed after cherry-pick.

This branch contains a single commit against current main:

  • typify-impl/src/convert.rs: narrow the Subschemas match arm from instance_type: _ to None | Some(SingleOrVec::Single(_)) so a multi-type Vec(_) instance_type falls through to the existing merge arm (try_merge_with_subschemas), which folds the outer type union into each subschema branch.
  • typify/tests/schemas/type-array-with-subschemas.{json,rs}: new fixture generated against upstream codegen, 8 cases (oneOf / anyOf / allOf / not + the singleton-type / rust-collisions regression guard + partially / fully unsatisfiable oneOf + simultaneous oneOf + allOf).

cargo test --workspace, cargo fmt --check, cargo clippy --all-targets all green locally.

Context on the fix itself is in #1000's description.

Closes #954.

…decomputer#954)

When a schema object had both `type: [a, b, c, ...]` and one of
`oneOf`/`anyOf`/`allOf`/`not` on the same level, convert_schema_object
matched an earlier arm that wildcarded the `instance_type` field
(flagged by a pre-existing TODO: "we probably shouldn't ignore this").
The subschema branches were then converted in isolation, silently
discarding the outer type union.

For the schema in issue oxidecomputer#954:

    {
      "type": ["string", "number", "boolean", "array"],
      "oneOf": [
        { "items": { "type": "string" } },
        { "items": { "type": "number" } },
        { "items": { "type": "boolean" } }
      ]
    }

typify produced an enum with only three array variants, losing the
string/number/boolean alternatives entirely.

The fix tightens the match pattern to `None | Some(Single(_))`, letting
`Some(Vec(_))` fall through to the existing merge arm which already
folds the outer schema into each subschema branch via
`try_merge_with_subschemas`. Single-type + subschema behaviour is
preserved, so tolerant handling of schemas whose outer type conflicts
with a branch (e.g. the rust-collisions fixture) is unchanged.

Adds a fixture with eight cases covering the affected code path:
  - `type:[...]` combined with oneOf, anyOf, allOf, and not
  - explicit `type: array` in every oneOf branch (primitives pruned)
  - partially and fully unsatisfiable oneOf
  - simultaneous oneOf + allOf
  - singleton-type regression guard (rust-collisions pattern)

No existing workspace fixture exercised `type: [...]` alongside
subschemas on the same object, so this fix had zero pre-existing
regression coverage.
Copy link
Copy Markdown
Collaborator

@ahl ahl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this. At least one test case is generating the wrong type.

For the $comments I'd ask for a little more brevity (are these LLM-written?).

#[serde(untagged)]
pub enum SingleTypeOneOfArrayBranch {
Object { kind: ::std::string::String },
Array([::std::string::String; 2usize]),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not valid

Comment thread typify-impl/src/convert.rs Outdated
Comment on lines +467 to +472
// Subschemas with no body validation and either no type or a
// single type. A multi-type (`Vec`) instance_type is deliberately
// excluded so that it flows through the merge arm below, which
// folds the type union into each subschema branch. Preserving the
// earlier behaviour for `None` / `Single` keeps existing tolerant
// handling of schemas whose outer type may conflict with a branch.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Subschemas with no body validation and either no type or a
// single type. A multi-type (`Vec`) instance_type is deliberately
// excluded so that it flows through the merge arm below, which
// folds the type union into each subschema branch. Preserving the
// earlier behaviour for `None` / `Single` keeps existing tolerant
// handling of schemas whose outer type may conflict with a branch.
// Subschemas with no additional validation and either no type or a
// single type. A multi-type (`Vec`) instance_type is deliberately
// excluded so that it flows through the merge arm below.

Per @ahl's review:

- Drop the `SingleTypeOneOfArrayBranch` test case. The schema's outer
  `type: "object"` makes the array branch unsatisfiable, yet codegen
  still emits the unreachable `Array([String; 2])` variant — i.e. the
  test was asserting on wrong output. The underlying behaviour
  (singleton outer type + conflicting branch type not pruned) is
  worth a separate look but isn't in scope for the oxidecomputer#954 fix.
- Trim verbose `$comment` rationales in the remaining cases.
- Shorten the match-arm comment in `convert.rs` per the inline
  suggestion. The match arm itself (`None | Some(SingleOrVec::Single(_))`)
  and the rest of the oxidecomputer#954 fix are unchanged.
@ndreno
Copy link
Copy Markdown
Author

ndreno commented May 13, 2026

Thanks for the review @ahl — pushed 5164bcb addressing all three points:

  • SingleTypeOneOfArrayBranch test case — dropped. You were right, the schema is contradictory (outer type: "object" makes the array branch unsatisfiable) and the generated Array([String; 2]) variant is unreachable. The case was originally meant as a "regression guard" for the pre-fix tolerant behaviour, but the behaviour being guarded is wrong, so keeping it would just be asserting on bad output.

  • $comment rationales — trimmed. (Yes, those were LLM-generated for me; lesson learned.) Surviving comments are one-liners and only on cases that aren't self-explanatory.

  • Match-arm doc comment — replaced with your suggested three-line version.

The match arm itself (None | Some(SingleOrVec::Single(_))) and the rest of the #954 fix are unchanged.

The underlying bug ahl's first point hints at — singleton outer type + a subschema branch with a conflicting explicit type, with the conflicting branch not getting pruned — is real and worth fixing, but feels out of scope for #954. Happy to follow up with a separate PR if that matches your preference, or fold it in here if you'd rather have it tracked together.

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.

Panic: Not yet implemented: unhandled not schema Object

2 participants