Skip to content

Commit 812fb29

Browse files
committed
feat: support xor and looseRecord for zod 4.2.0
1 parent 2409850 commit 812fb29

8 files changed

Lines changed: 305 additions & 137 deletions

File tree

lib/dezerialize.ts

Lines changed: 69 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ import {
1010
SzUnion,
1111
SzDiscriminatedUnion,
1212
SzIntersection,
13+
SzXor,
1314
SzTuple,
1415
SzRecord,
16+
SzLooseRecord,
1517
SzMap,
1618
SzSet,
1719
SzEnum,
@@ -147,48 +149,56 @@ export type Dezerialize<T extends SzType | SzRef> = T extends SzRef
147149
Dezerialize<Key>,
148150
Dezerialize<Value>
149151
>
150-
: T extends SzMap<
152+
: T extends SzLooseRecord<
151153
infer Key,
152154
infer Value
153155
>
154-
? z.ZodMap<
156+
? z.ZodRecord<
155157
Dezerialize<Key>,
156158
Dezerialize<Value>
157-
> // Enum
158-
: T extends SzEnum<
159-
infer Values
159+
>
160+
: T extends SzMap<
161+
infer Key,
162+
infer Value
160163
>
161-
? z.ZodEnum<Values> // Union/Intersection
162-
: T extends SzUnion<
163-
infer _Options
164+
? z.ZodMap<
165+
Dezerialize<Key>,
166+
Dezerialize<Value>
167+
> // Enum
168+
: T extends SzEnum<
169+
infer Values
164170
>
165-
? z.ZodUnion<any>
166-
: T extends SzDiscriminatedUnion<
167-
infer Discriminator,
171+
? z.ZodEnum<Values> // Union/Intersection
172+
: T extends SzUnion<
168173
infer _Options
169174
>
170-
? z.ZodDiscriminatedUnion<any>
171-
: T extends SzIntersection<
172-
infer L,
173-
infer R
175+
? z.ZodUnion<any>
176+
: T extends SzDiscriminatedUnion<
177+
infer Discriminator,
178+
infer _Options
174179
>
175-
? z.ZodIntersection<
176-
Dezerialize<L>,
177-
Dezerialize<R>
178-
> // Specials
179-
: T extends SzPromise<
180-
infer Value
181-
>
182-
? z.ZodPromise<
183-
Dezerialize<Value>
180+
? z.ZodDiscriminatedUnion<any>
181+
: T extends SzIntersection<
182+
infer L,
183+
infer R
184184
>
185-
: T extends SzCatch<
185+
? z.ZodIntersection<
186+
Dezerialize<L>,
187+
Dezerialize<R>
188+
> // Specials
189+
: T extends SzPromise<
186190
infer Value
187191
>
188-
? z.ZodCatch<
192+
? z.ZodPromise<
189193
Dezerialize<Value>
190194
>
191-
: any; // unknown;
195+
: T extends SzCatch<
196+
infer Value
197+
>
198+
? z.ZodCatch<
199+
Dezerialize<Value>
200+
>
201+
: any; // unknown;
192202

193203
type DezerializersMap = {
194204
[T in SzType["type"]]: (
@@ -535,6 +545,23 @@ const dezerializers = {
535545
opts.pathToSchema.set(opts.path, i);
536546
return getCustomChecks(i, shape, opts);
537547
}) as any,
548+
looseRecord: ((shape: SzLooseRecord, opts: DezerializerOptions) => {
549+
const i = z.looseRecord(
550+
checkRef(shape.key, opts) ||
551+
(d(shape.key, {
552+
...opts,
553+
path: opts.path + "/key",
554+
}) as z.ZodString | z.ZodNumber | z.ZodSymbol),
555+
checkRef(shape.value, opts) ||
556+
d(shape.value, {
557+
...opts,
558+
path: opts.path + "/value",
559+
}),
560+
getError(shape, opts),
561+
);
562+
opts.pathToSchema.set(opts.path, i);
563+
return getCustomChecks(i, shape, opts);
564+
}) as any,
538565
map: ((shape: SzMap<any, any>, opts: DezerializerOptions) => {
539566
const i = z.map(
540567
checkRef(shape.key, opts) ||
@@ -591,6 +618,21 @@ const dezerializers = {
591618
opts.pathToSchema.set(opts.path, i);
592619
return getCustomChecks(i, shape, opts);
593620
}) as any,
621+
xor: ((shape: SzXor, opts: DezerializerOptions) => {
622+
const i = z.xor(
623+
shape.options.map(
624+
(opt, idx) =>
625+
checkRef(opt, opts) ||
626+
d(opt, {
627+
...opts,
628+
path: opts.path + "/options/" + idx,
629+
}),
630+
) as any,
631+
getError(shape, opts),
632+
);
633+
opts.pathToSchema.set(opts.path, i);
634+
return getCustomChecks(i, shape, opts);
635+
}) as any,
594636
intersection: ((shape: SzIntersection, opts: DezerializerOptions) => {
595637
const i = z.intersection(
596638
checkRef(shape.left, opts) ||

lib/index.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,50 @@ test.each([
481481
key: { type: "string" },
482482
value: { type: "literal", values: [42] },
483483
}),
484+
p(
485+
z.xor([
486+
z.object({ type: z.literal("user"), name: z.string() }),
487+
z.object({ type: z.literal("admin"), role: z.string() }),
488+
]),
489+
{
490+
type: "xor",
491+
options: [
492+
{
493+
type: "object",
494+
properties: {
495+
type: {
496+
type: "literal",
497+
values: ["user"],
498+
},
499+
name: {
500+
type: "string",
501+
},
502+
},
503+
},
504+
{
505+
type: "object",
506+
properties: {
507+
type: {
508+
type: "literal",
509+
values: ["admin"],
510+
},
511+
role: {
512+
type: "string",
513+
},
514+
},
515+
},
516+
],
517+
},
518+
),
519+
p(z.looseRecord(z.string(), z.number()), {
520+
type: "looseRecord",
521+
key: {
522+
type: "string",
523+
},
524+
value: {
525+
type: "number",
526+
},
527+
}),
484528
p(z.map(z.number(), z.string()), {
485529
type: "map",
486530
key: { type: "number" },

lib/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,14 @@
3333
},
3434
"dependencies": {
3535
"type-fest": "^4.41.0",
36-
"zod": "^4.1.13"
36+
"zod": "^4.2.0"
3737
},
3838
"devDependencies": {
39-
"@vitest/coverage-v8": "^4.0.13",
40-
"prettier": "3.6.2",
39+
"@vitest/coverage-v8": "^4.0.15",
40+
"prettier": "3.7.4",
4141
"tsup": "^8.5.1",
4242
"typescript": "5.9.3",
43-
"vitest": "4.0.13"
43+
"vitest": "4.0.15"
4444
},
4545
"files": [
4646
"dist/*"

lib/schema.zodex.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -662,6 +662,22 @@
662662
}
663663
}
664664
},
665+
{
666+
"description": "XOR",
667+
"type": "object",
668+
"properties": {
669+
"type": {
670+
"type": "literal",
671+
"values": ["xor"]
672+
},
673+
"options": {
674+
"type": "array",
675+
"element": {
676+
"$ref": "#/$defs/reference-or-type"
677+
}
678+
}
679+
}
680+
},
665681
{
666682
"$ref": "#/$defs/tuple"
667683
},
@@ -681,6 +697,22 @@
681697
}
682698
}
683699
},
700+
{
701+
"description": "Loose Record",
702+
"type": "object",
703+
"properties": {
704+
"type": {
705+
"type": "literal",
706+
"values": ["looseRecord"]
707+
},
708+
"key": {
709+
"$ref": "#/$defs/key"
710+
},
711+
"value": {
712+
"$ref": "#/$defs/reference-or-type"
713+
}
714+
}
715+
},
684716
{
685717
"description": "Map",
686718
"type": "object",

lib/types.ts

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,10 @@ export type SzIntersection<
195195
left: Left;
196196
right: Right;
197197
};
198+
export type SzXor<Options extends [SzType, ...SzType[]] = [SzType]> = {
199+
type: "xor";
200+
options: Options;
201+
};
198202
export type SzTuple<
199203
Items extends [SzType, ...SzType[]] | [] = [SzType, ...SzType[]] | [],
200204
> = {
@@ -210,6 +214,14 @@ export type SzRecord<
210214
key: Key;
211215
value: Value;
212216
};
217+
export type SzLooseRecord<
218+
Key extends SzKey = SzKey,
219+
Value extends SzType = SzType,
220+
> = {
221+
type: "looseRecord";
222+
key: Key;
223+
value: Value;
224+
};
213225
export type SzMap<Key extends SzType, Value extends SzType> = {
214226
type: "map";
215227
key: Key;
@@ -311,8 +323,10 @@ export type SzType = (
311323
| SzUnion<any>
312324
| SzDiscriminatedUnion<any, any>
313325
| SzIntersection<any, any>
326+
| SzXor<any>
314327
| SzTuple<any>
315328
| SzRecord<any, any>
329+
| SzLooseRecord<any, any>
316330
| SzMap<any, any>
317331
| SzSet<any>
318332
| SzEnum<any>
@@ -336,14 +350,18 @@ export type SzUnionize<T extends SzType | SzRef> =
336350
? SzUnionize<Options[number]>
337351
: T extends SzIntersection<infer L, infer R>
338352
? SzUnionize<L | R>
339-
: T extends SzTuple<infer Items>
340-
? SzUnionize<Items[number]>
341-
: T extends SzRecord<infer _Key, infer Value>
342-
? SzUnionize<Value>
343-
: T extends SzMap<infer _Key, infer Value>
353+
: T extends SzXor<infer Options>
354+
? SzUnionize<Options[number]>
355+
: T extends SzTuple<infer Items>
356+
? SzUnionize<Items[number]>
357+
: T extends SzRecord<infer _Key, infer Value>
344358
? SzUnionize<Value>
345-
: T extends SzSet<infer T>
346-
? SzUnionize<T>
347-
: T extends SzPromise<infer Value>
359+
: T extends SzLooseRecord<infer _Key, infer Value>
360+
? SzUnionize<Value>
361+
: T extends SzMap<infer _Key, infer Value>
348362
? SzUnionize<Value>
349-
: never);
363+
: T extends SzSet<infer T>
364+
? SzUnionize<T>
365+
: T extends SzPromise<infer Value>
366+
? SzUnionize<Value>
367+
: never);

0 commit comments

Comments
 (0)