zod icon indicating copy to clipboard operation
zod copied to clipboard

Union type generates anyOf instead oneOf

Open de-don opened this issue 1 month ago • 5 comments

export const example = z.union([
  z.string().min(10),
  z.string().min(5)
]);

will generate schema like this in [email protected]

"exampleField": {
  "anyOf":[
    {"type":"string","minLength":10},
    {"type":"string","minLength":5}
  ]
}

but in zod v3 it generates:

"exampleField": {
  "oneOf":[
    {"type":"string","minLength":10},
    {"type":"string","minLength":5}
  ]
}

I think it should use oneOf instead of anyOf

de-don avatar Dec 02 '25 10:12 de-don

Duplicate Detection

ℹ️ This issue may be similar to colinhacks/zod#5493.

Related Issues

  • colinhacks/zod#5493: Union type generates anyOf instead oneOf

Suggested Issue Assignees

  • ddontsov-b2broker

Actions

  • [ ] 📝 Generate an implementation plan for this issue

ℹ️ Note: Issue enrichment is currently in early access.

Disable automatic issue enrichment

To disable automatic issue enrichment, add the following to your .coderabbit.yaml:

issue_enrichment:
  auto_enrich:
    enabled: false

coderabbitai[bot] avatar Dec 02 '25 10:12 coderabbitai[bot]

I just recreated this from another account, and closed previous issue, sorry. This is a right one

de-don avatar Dec 02 '25 10:12 de-don

Hi @de-don! I'm Dosu and I’m helping the zod team.

This change in Zod v4 is intentional: unions now generate JSON Schema using anyOf instead of oneOf because Zod unions are non-exclusive—multiple schemas can match the same value—so anyOf is the correct representation according to the JSON Schema spec. In contrast, oneOf requires that exactly one schema matches, which is only guaranteed for discriminated unions (where a specific field distinguishes each variant). For discriminated unions, Zod v4 does use oneOf as expected. You can see the rationale and implementation details in this merged PR and related issue discussion.

There’s no built-in config to switch regular unions back to oneOf, but you can use the override option in z.toJSONSchema() to manually adjust the output. For example:

z.toJSONSchema(schema, {
  override(ctx) {
    if (ctx.jsonSchema.anyOf) {
      ctx.jsonSchema.oneOf = ctx.jsonSchema.anyOf;
      delete ctx.jsonSchema.anyOf;
    }
  }
});

If this answers your question, feel free to close the issue! If you need more details or have follow-ups, let me know.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other

Chat with Dosu Open in Cursor Join Discord Share on X

dosubot[bot] avatar Dec 02 '25 10:12 dosubot[bot]

@dosubot, how should I proceed with the case, when I need union from string and int?

de-don avatar Dec 02 '25 11:12 de-don

For a union of string and int in Zod v4, use z.union([z.string(), z.number().int()]). When you convert this to JSON Schema with z.toJSONSchema(), Zod will generate an anyOf array, not oneOf. This is intentional—Zod unions are non-exclusive, so anyOf matches the semantics of Zod unions and the JSON Schema spec. oneOf is only used for discriminated unions, where exactly one schema matches due to a discriminator field. You can see this logic in the Zod codebase and in issue discussions.

If you need the output to use oneOf instead, you can post-process the schema using the override option:

const schema = z.union([z.string(), z.number().int()]);
const jsonSchema = z.toJSONSchema(schema, {
  override(ctx) {
    if (ctx.jsonSchema.anyOf) {
      ctx.jsonSchema.oneOf = ctx.jsonSchema.anyOf;
      delete ctx.jsonSchema.anyOf;
    }
  }
});

More details are in the Zod JSON Schema docs and related issues. If this solves your problem, you can close the issue. If you have more questions, let me know!

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other

Chat with Dosu Open in Cursor Join Discord Share on X

dosubot[bot] avatar Dec 02 '25 11:12 dosubot[bot]

@dosubot On [email protected] when using zod/v4 using discriminatedUnion outputs anyOf instead of oneOf as mentioned here. Why?

enzoferey avatar Dec 14 '25 23:12 enzoferey

Discriminated union -> oneOf Union -> anyOf

This is sound and mirrors the validation behavior of these keywords in JSON Schema. (With discriminated unions, the input can only match one of the union elements so it's implicitly an exclusive union)

colinhacks avatar Dec 15 '25 07:12 colinhacks

@colinhacks is it possible somehow to use discriminatedUnion with simple types like string, number, boolean? In our case, for example, we have 2 types of string like uuid_id and numeric_id, and we need union uuid_id | numeric_id.

Now I fixed this via "patching" the generated swagger and replacing anyOf -> oneOf because swagger codegeneration works really bad with anyOf :(

de-don avatar Dec 15 '25 09:12 de-don