Skip to content

Conversation

@cruzdanilo
Copy link
Contributor

this pr fixes the json schema conversion for variant types. it was using anyOf, now it correctly uses oneOf.

valibot's variant type requires the data to match exactly one of the specified schemas. in json schema, oneOf enforces this constraint. anyOf allows data to match one or more schemas, which isn't the correct behavior for variant.

json schema docs for oneOf: JSON Schema Validation - oneOf

for pure json schema, oneOf is the standard for this. in openapi, this can be complemented by a discriminator object (spec), which helps map to the correct schema when oneOf or anyOf are used for polymorphism.

for openapi, the key from v.variant(key, options) could directly be used as discriminator.propertyName. this would help tools pick the right schema within the oneOf based on that key's value. supporting this would be a useful extension for @valibot/to-json-schema for users targeting openapi, even though discriminator is an openapi-specific keyword.

Copilot AI review requested due to automatic review settings May 13, 2025 01:25
@vercel
Copy link

vercel bot commented May 13, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
valibot ✅ Ready (Inspect) Visit Preview 💬 Add feedback May 13, 2025 1:30am

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR fixes the JSON schema conversion for variant types by replacing the use of "anyOf" with "oneOf", ensuring that variant data matches exactly one schema as required by valibot.

  • Separates the handling of "union" and "variant" schemas in convertSchema
  • Updates tests to reflect the new "oneOf" behavior for variant types

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
packages/to-json-schema/src/convertSchema.ts Splits variant handling into its own case using "oneOf" instead of "anyOf"
packages/to-json-schema/src/convertSchema.test.ts Adjusts test expectations to check for "oneOf" for variant schemas

@fabian-hiller
Copy link
Member

Thank you for creating this PR! Do other libraries like Zod's JSON schema conversion use oneOf too? I am concerned that oneOf is stricter and may cause errors when building nested variant schemas where an input may match two or more options.

@cruzdanilo
Copy link
Contributor Author

yes, zod-openapi converts discriminatedUnion to oneOf and also adds the discriminator.
i'm using zod-openapi as a reference because it's the converter used by hono-openapi, same library i'm using with @valibot/to-json-schema.

https://github.com/samchungy/zod-openapi/blob/master/src/create/schema/parsers/discriminatedUnion.ts#L21-L48

@fabian-hiller
Copy link
Member

I am concerned that oneOf is stricter and may cause errors when building nested variant schemas where an input may match two or more options.

What do you think about this? Here is a nested variant example.

@cruzdanilo
Copy link
Contributor Author

thanks for the example. i get your concern about nested variants and oneOf's strictness.

v.variant() means "exactly one option must match," and json schema's oneOf mirrors this. if an input could match multiple options in a v.variant() call, it suggests an ambiguity in that variant's definition, as it's meant to pick one schema via its discriminator key.

your nested example:

const ComplexVariantSchema = v.variant('kind', [ // outer oneOf on 'kind'
  v.variant('type', [ // inner oneOf on 'type' for 'fruit'
    v.object({ kind: v.literal('fruit'), type: v.literal('apple'), /*...*/ }),
    v.object({ kind: v.literal('fruit'), type: v.literal('banana'), /*...*/ }),
  ]),
  v.variant('type', [ // inner oneOf on 'type' for 'vegetable'
    v.object({ kind: v.literal('vegetable'), type: v.literal('carrot'), /*...*/ }),
    v.object({ kind: v.literal('vegetable'), type: v.literal('tomato'), /*...*/ }),
  ]),
]);

this translates correctly to nested oneOfs: outer for 'kind', then inner for 'type' within each branch.

if an input like { kind: 'fruit', type: 'apple', ... } also satisfied { kind: 'fruit', type: 'banana', ... } within the first inner v.variant('type', ...), that inner variant isn't truly discriminating on 'type'.

anyOf would allow this ambiguity, misrepresenting v.variant()'s stricter intent. the goal is to accurately model valibot's semantics. if a variant schema allows multiple legitimate matches for its direct options, v.union() might be a better fit for that part of the structure.

so, oneOf's strictness is why it's the right mapping for variant's "exactly one" rule, even nested. each variant() call has its own scope of mutual exclusivity.

@fabian-hiller fabian-hiller self-assigned this Jun 11, 2025
@fabian-hiller fabian-hiller added the fix A smaller enhancement or bug fix label Jun 11, 2025
@fabian-hiller fabian-hiller added this to the v1.4 (to-json-schema) milestone Jun 11, 2025
@vladshcherbin
Copy link
Contributor

@fabian-hiller any chance to merge this one? such an easy fix which helps with better schema

@fabian-hiller
Copy link
Member

fabian-hiller commented Sep 16, 2025

@cruzdanilo sorry that I did not answer on your last message. 😐 It is sometimes hard to find the right balance between doing community work, working on issues and PRs but also make time for working on my own ideas and projects.

@vladshcherbin I think I will merge that with the next minor or major release. Does this block you or causes issues in some of your projects?

@vladshcherbin
Copy link
Contributor

@fabian-hiller no blocks really, just a more logically correct and pleasing openapi specs generation

I know it's hard to find time for all projects and life, just slightly miss the time when you fired updates like from a minigun. That's what I absolutely loved after switching from stale zod. Keeping fingers crossed that you'll find some time to review PRs - some of them have minimal changes (like this one) but are so welcome to have ♥️

@fabian-hiller
Copy link
Member

I see. While I did my master's degree I kind of worked on Valibot full time. That's why I could move so fast. I know there is still a lot to do but I also know that the library is already in a good state. That's why I was focusing on Formisch in the last 3 months. I will probably focus on Formisch for one more month before I start working on Valibot 1.2 and Valibot v2.

@vercel
Copy link

vercel bot commented Dec 3, 2025

@fabian-hiller is attempting to deploy a commit to the Valibot Team on Vercel.

A member of the Team first needs to authorize it.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Dec 3, 2025

Open in StackBlitz

npm i https://pkg.pr.new/valibot@1193

commit: d18247d

Copilot finished reviewing on behalf of fabian-hiller December 3, 2025 02:33
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@fabian-hiller fabian-hiller merged commit 838cad3 into open-circle:main Dec 3, 2025
12 of 13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix A smaller enhancement or bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants