zod 4.2.0-canary.20251118T055019 → 4.2.0-canary.20251118T055142

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zod",
3
- "version": "4.2.0-canary.20251118T055019",
3
+ "version": "4.2.0-canary.20251118T055142",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "author": "Colin McDonnell <zod@colinhacks.com>",
@@ -706,6 +706,54 @@ describe("toJSONSchema", () => {
706
706
  `);
707
707
  });
708
708
 
709
+ test("discriminated unions", () => {
710
+ const schema = z.discriminatedUnion("type", [
711
+ z.object({ type: z.literal("success"), data: z.string() }),
712
+ z.object({ type: z.literal("error"), message: z.string() }),
713
+ ]);
714
+ expect(z.toJSONSchema(schema)).toMatchInlineSnapshot(`
715
+ {
716
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
717
+ "oneOf": [
718
+ {
719
+ "additionalProperties": false,
720
+ "properties": {
721
+ "data": {
722
+ "type": "string",
723
+ },
724
+ "type": {
725
+ "const": "success",
726
+ "type": "string",
727
+ },
728
+ },
729
+ "required": [
730
+ "type",
731
+ "data",
732
+ ],
733
+ "type": "object",
734
+ },
735
+ {
736
+ "additionalProperties": false,
737
+ "properties": {
738
+ "message": {
739
+ "type": "string",
740
+ },
741
+ "type": {
742
+ "const": "error",
743
+ "type": "string",
744
+ },
745
+ },
746
+ "required": [
747
+ "type",
748
+ "message",
749
+ ],
750
+ "type": "object",
751
+ },
752
+ ],
753
+ }
754
+ `);
755
+ });
756
+
709
757
  test("intersections", () => {
710
758
  const schema = z.intersection(z.object({ name: z.string() }), z.object({ age: z.number() }));
711
759
 
@@ -330,13 +330,20 @@ export class JSONSchemaGenerator {
330
330
  }
331
331
  case "union": {
332
332
  const json: JSONSchema.BaseSchema = _json as any;
333
+ // Discriminated unions use oneOf (exactly one match) instead of anyOf (one or more matches)
334
+ // because the discriminator field ensures mutual exclusivity between options in JSON Schema
335
+ const isDiscriminated = (def as any).discriminator !== undefined;
333
336
  const options = def.options.map((x, i) =>
334
337
  this.process(x, {
335
338
  ...params,
336
- path: [...params.path, "anyOf", i],
339
+ path: [...params.path, isDiscriminated ? "oneOf" : "anyOf", i],
337
340
  })
338
341
  );
339
- json.anyOf = options;
342
+ if (isDiscriminated) {
343
+ json.oneOf = options;
344
+ } else {
345
+ json.anyOf = options;
346
+ }
340
347
  break;
341
348
  }
342
349
  case "intersection": {
@@ -252,11 +252,19 @@ class JSONSchemaGenerator {
252
252
  }
253
253
  case "union": {
254
254
  const json = _json;
255
+ // Discriminated unions use oneOf (exactly one match) instead of anyOf (one or more matches)
256
+ // because the discriminator field ensures mutual exclusivity between options in JSON Schema
257
+ const isDiscriminated = def.discriminator !== undefined;
255
258
  const options = def.options.map((x, i) => this.process(x, {
256
259
  ...params,
257
- path: [...params.path, "anyOf", i],
260
+ path: [...params.path, isDiscriminated ? "oneOf" : "anyOf", i],
258
261
  }));
259
- json.anyOf = options;
262
+ if (isDiscriminated) {
263
+ json.oneOf = options;
264
+ }
265
+ else {
266
+ json.anyOf = options;
267
+ }
260
268
  break;
261
269
  }
262
270
  case "intersection": {
@@ -248,11 +248,19 @@ export class JSONSchemaGenerator {
248
248
  }
249
249
  case "union": {
250
250
  const json = _json;
251
+ // Discriminated unions use oneOf (exactly one match) instead of anyOf (one or more matches)
252
+ // because the discriminator field ensures mutual exclusivity between options in JSON Schema
253
+ const isDiscriminated = def.discriminator !== undefined;
251
254
  const options = def.options.map((x, i) => this.process(x, {
252
255
  ...params,
253
- path: [...params.path, "anyOf", i],
256
+ path: [...params.path, isDiscriminated ? "oneOf" : "anyOf", i],
254
257
  }));
255
- json.anyOf = options;
258
+ if (isDiscriminated) {
259
+ json.oneOf = options;
260
+ }
261
+ else {
262
+ json.anyOf = options;
263
+ }
256
264
  break;
257
265
  }
258
266
  case "intersection": {