Skip to content

Commit e17dcb6

Browse files
authored
Add z.fromJSONSchema(), z.looseRecord(), z.xor() (#5534)
* Initial version of z.fromJSONSchema * Add z.fromJSONSchema * Address reviews * Update * Make XOR a subclass of ZodUnion * Update docs * Tweak docs * Update docs * Tweaks * Update test
1 parent d632df3 commit e17dcb6

File tree

17 files changed

+1743
-186
lines changed

17 files changed

+1743
-186
lines changed

packages/docs/content/api.mdx

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1700,6 +1700,38 @@ console.log(optionalUrl.safeParse("not a valid url").success); // false
17001700
17011701
<br/> */}
17021702

1703+
## Exclusive unions (XOR)
1704+
1705+
An exclusive union (XOR) is a union where exactly one option must match. Unlike regular unions that succeed when any option matches, `z.xor()` fails if zero options match OR if multiple options match.
1706+
1707+
```ts
1708+
const schema = z.xor([z.string(), z.number()]);
1709+
1710+
schema.parse("hello"); // ✅ passes
1711+
schema.parse(42); // ✅ passes
1712+
schema.parse(true); // ❌ fails (zero matches)
1713+
```
1714+
1715+
This is useful when you want to ensure mutual exclusivity between options:
1716+
1717+
```ts
1718+
// Validate that exactly ONE of these matches
1719+
const payment = z.xor([
1720+
z.object({ type: z.literal("card"), cardNumber: z.string() }),
1721+
z.object({ type: z.literal("bank"), accountNumber: z.string() }),
1722+
]);
1723+
1724+
payment.parse({ type: "card", cardNumber: "1234" }); // ✅ passes
1725+
```
1726+
1727+
If the input could match multiple options, `z.xor()` will fail:
1728+
1729+
```ts
1730+
const overlapping = z.xor([z.string(), z.any()]);
1731+
overlapping.parse("hello"); // ❌ fails (matches both string and any)
1732+
```
1733+
1734+
17031735
## Discriminated unions
17041736

17051737
A [discriminated union](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions) is a special kind of union in which a) all the options are object schemas that b) share a particular key (the "discriminator"). Based on the value of the discriminator key, TypeScript is able to "narrow" the type signature as you'd expect.
@@ -1768,6 +1800,7 @@ Each option should be an *object schema* whose discriminator prop (`status` in t
17681800
</Accordion>
17691801
</Accordions>
17701802

1803+
17711804
## Intersections
17721805

17731806
Intersection types (`A & B`) represent a logical "AND".
@@ -1802,6 +1835,8 @@ type EmployedPerson = z.infer<typeof EmployedPerson>;
18021835

18031836
Record schemas are used to validate types such as `Record<string, string>`.
18041837

1838+
### `z.record`
1839+
18051840
```ts
18061841
const IdCache = z.record(z.string(), z.string());
18071842
type IdCache = z.infer<typeof IdCache>; // Record<string, string>
@@ -1828,6 +1863,9 @@ const Person = z.record(Keys, z.string());
18281863
// { id: string; name: string; email: string }
18291864
```
18301865

1866+
### `z.partialRecord`
1867+
1868+
18311869
<Callout>
18321870
**Zod 4** — In Zod 4, if you pass a `z.enum` as the first argument to `z.record()`, Zod will exhaustively check that all enum values exist in the input as keys. This behavior agrees with TypeScript:
18331871

@@ -1849,6 +1887,23 @@ const Person = z.partialRecord(Keys, z.string());
18491887
// { id?: string; name?: string; email?: string }
18501888
```
18511889

1890+
### `z.looseRecord`
1891+
1892+
By default, `z.record()` errors on keys that don't match the key schema. Use `z.looseRecord()` to pass through non-matching keys unchanged. This is particularly useful when combined with intersections to model multiple pattern properties:
1893+
1894+
```ts
1895+
const schema = z.object({ name: z.string() }).passthrough()
1896+
.and(z.looseRecord(z.string().regex(/^S_/), z.string()))
1897+
.and(z.looseRecord(z.string().regex(/^N_/), z.number()));
1898+
1899+
schema.parse({
1900+
name: "John",
1901+
other: "value", // passes through unchanged
1902+
S_foo: "bar", // validated as string
1903+
N_count: 123, // validated as number
1904+
});
1905+
```
1906+
18521907
<Accordions>
18531908
<Accordion title="A note on numeric keys">
18541909

0 commit comments

Comments
 (0)