zod 4.1.0-canary.20250821T014930 → 4.1.0-canary.20250823T071040

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.
Files changed (158) hide show
  1. package/index.cjs +17 -7
  2. package/package.json +1 -1
  3. package/src/v4/classic/external.ts +0 -1
  4. package/src/v4/classic/parse.ts +49 -0
  5. package/src/v4/classic/schemas.ts +145 -7
  6. package/src/v4/classic/tests/catch.test.ts +25 -0
  7. package/src/v4/classic/tests/codec-examples.test.ts +538 -0
  8. package/src/v4/classic/tests/codec.test.ts +532 -0
  9. package/src/v4/classic/tests/continuability.test.ts +1 -1
  10. package/src/v4/classic/tests/default.test.ts +32 -0
  11. package/src/v4/classic/tests/firstparty.test.ts +4 -0
  12. package/src/v4/classic/tests/function.test.ts +31 -31
  13. package/src/v4/classic/tests/hash.test.ts +68 -0
  14. package/src/v4/classic/tests/nonoptional.test.ts +15 -0
  15. package/src/v4/classic/tests/object.test.ts +31 -0
  16. package/src/v4/classic/tests/pipe.test.ts +25 -5
  17. package/src/v4/classic/tests/prefault.test.ts +25 -0
  18. package/src/v4/classic/tests/preprocess.test.ts +1 -6
  19. package/src/v4/classic/tests/refine.test.ts +76 -3
  20. package/src/v4/classic/tests/string-formats.test.ts +16 -0
  21. package/src/v4/classic/tests/string.test.ts +82 -1
  22. package/src/v4/classic/tests/stringbool.test.ts +40 -0
  23. package/src/v4/classic/tests/template-literal.test.ts +1 -1
  24. package/src/v4/classic/tests/to-json-schema.test.ts +21 -2
  25. package/src/v4/classic/tests/transform.test.ts +7 -0
  26. package/src/v4/classic/tests/union.test.ts +1 -1
  27. package/src/v4/core/api.ts +25 -35
  28. package/src/v4/core/core.ts +7 -26
  29. package/src/v4/core/index.ts +0 -1
  30. package/src/v4/core/json-schema.ts +1 -0
  31. package/src/v4/core/parse.ts +101 -0
  32. package/src/v4/core/regexes.ts +40 -1
  33. package/src/v4/core/schemas.ts +521 -129
  34. package/src/v4/core/to-json-schema.ts +43 -8
  35. package/src/v4/core/util.ts +73 -0
  36. package/src/v4/mini/external.ts +0 -1
  37. package/src/v4/mini/parse.ts +14 -1
  38. package/src/v4/mini/schemas.ts +153 -12
  39. package/src/v4/mini/tests/codec.test.ts +499 -0
  40. package/src/v4/mini/tests/object.test.ts +9 -0
  41. package/src/v4/mini/tests/string.test.ts +16 -0
  42. package/v3/index.cjs +17 -7
  43. package/v4/classic/coerce.cjs +17 -7
  44. package/v4/classic/compat.cjs +17 -7
  45. package/v4/classic/errors.cjs +17 -7
  46. package/v4/classic/external.cjs +18 -9
  47. package/v4/classic/external.d.cts +1 -1
  48. package/v4/classic/external.d.ts +1 -1
  49. package/v4/classic/external.js +1 -1
  50. package/v4/classic/index.cjs +17 -7
  51. package/v4/classic/iso.cjs +17 -7
  52. package/v4/classic/parse.cjs +27 -8
  53. package/v4/classic/parse.d.cts +8 -0
  54. package/v4/classic/parse.d.ts +8 -0
  55. package/v4/classic/parse.js +9 -0
  56. package/v4/classic/schemas.cjs +76 -11
  57. package/v4/classic/schemas.d.cts +48 -2
  58. package/v4/classic/schemas.d.ts +48 -2
  59. package/v4/classic/schemas.js +51 -3
  60. package/v4/core/api.cjs +36 -35
  61. package/v4/core/api.d.cts +3 -4
  62. package/v4/core/api.d.ts +3 -4
  63. package/v4/core/api.js +19 -24
  64. package/v4/core/checks.cjs +17 -7
  65. package/v4/core/core.cjs +8 -1
  66. package/v4/core/core.d.cts +3 -0
  67. package/v4/core/core.d.ts +3 -0
  68. package/v4/core/core.js +6 -0
  69. package/v4/core/errors.cjs +17 -7
  70. package/v4/core/index.cjs +17 -8
  71. package/v4/core/index.d.cts +0 -1
  72. package/v4/core/index.d.ts +0 -1
  73. package/v4/core/index.js +0 -1
  74. package/v4/core/json-schema.d.cts +1 -0
  75. package/v4/core/json-schema.d.ts +1 -0
  76. package/v4/core/parse.cjs +62 -8
  77. package/v4/core/parse.d.cts +24 -0
  78. package/v4/core/parse.d.ts +24 -0
  79. package/v4/core/parse.js +36 -0
  80. package/v4/core/regexes.cjs +34 -2
  81. package/v4/core/regexes.d.cts +16 -0
  82. package/v4/core/regexes.d.ts +16 -0
  83. package/v4/core/regexes.js +32 -1
  84. package/v4/core/schemas.cjs +326 -84
  85. package/v4/core/schemas.d.cts +61 -3
  86. package/v4/core/schemas.d.ts +61 -3
  87. package/v4/core/schemas.js +308 -76
  88. package/v4/core/to-json-schema.cjs +42 -5
  89. package/v4/core/to-json-schema.d.cts +4 -3
  90. package/v4/core/to-json-schema.d.ts +4 -3
  91. package/v4/core/to-json-schema.js +42 -5
  92. package/v4/core/util.cjs +69 -0
  93. package/v4/core/util.d.cts +10 -0
  94. package/v4/core/util.d.ts +10 -0
  95. package/v4/core/util.js +62 -0
  96. package/v4/locales/ar.cjs +17 -7
  97. package/v4/locales/az.cjs +17 -7
  98. package/v4/locales/be.cjs +17 -7
  99. package/v4/locales/bg.cjs +17 -7
  100. package/v4/locales/ca.cjs +17 -7
  101. package/v4/locales/cs.cjs +17 -7
  102. package/v4/locales/da.cjs +17 -7
  103. package/v4/locales/de.cjs +17 -7
  104. package/v4/locales/en.cjs +17 -7
  105. package/v4/locales/eo.cjs +17 -7
  106. package/v4/locales/es.cjs +17 -7
  107. package/v4/locales/fa.cjs +17 -7
  108. package/v4/locales/fi.cjs +17 -7
  109. package/v4/locales/fr-CA.cjs +17 -7
  110. package/v4/locales/fr.cjs +17 -7
  111. package/v4/locales/he.cjs +17 -7
  112. package/v4/locales/hu.cjs +17 -7
  113. package/v4/locales/id.cjs +17 -7
  114. package/v4/locales/is.cjs +17 -7
  115. package/v4/locales/it.cjs +17 -7
  116. package/v4/locales/ja.cjs +17 -7
  117. package/v4/locales/kh.cjs +17 -7
  118. package/v4/locales/ko.cjs +17 -7
  119. package/v4/locales/mk.cjs +17 -7
  120. package/v4/locales/ms.cjs +17 -7
  121. package/v4/locales/nl.cjs +17 -7
  122. package/v4/locales/no.cjs +17 -7
  123. package/v4/locales/ota.cjs +17 -7
  124. package/v4/locales/pl.cjs +17 -7
  125. package/v4/locales/ps.cjs +17 -7
  126. package/v4/locales/pt.cjs +17 -7
  127. package/v4/locales/ru.cjs +17 -7
  128. package/v4/locales/sl.cjs +17 -7
  129. package/v4/locales/sv.cjs +17 -7
  130. package/v4/locales/ta.cjs +17 -7
  131. package/v4/locales/th.cjs +17 -7
  132. package/v4/locales/tr.cjs +17 -7
  133. package/v4/locales/ua.cjs +17 -7
  134. package/v4/locales/ur.cjs +17 -7
  135. package/v4/locales/vi.cjs +17 -7
  136. package/v4/locales/yo.cjs +17 -7
  137. package/v4/locales/zh-CN.cjs +17 -7
  138. package/v4/locales/zh-TW.cjs +17 -7
  139. package/v4/mini/coerce.cjs +17 -7
  140. package/v4/mini/external.cjs +18 -9
  141. package/v4/mini/external.d.cts +1 -1
  142. package/v4/mini/external.d.ts +1 -1
  143. package/v4/mini/external.js +1 -1
  144. package/v4/mini/index.cjs +17 -7
  145. package/v4/mini/iso.cjs +17 -7
  146. package/v4/mini/parse.cjs +9 -1
  147. package/v4/mini/parse.d.cts +1 -1
  148. package/v4/mini/parse.d.ts +1 -1
  149. package/v4/mini/parse.js +1 -1
  150. package/v4/mini/schemas.cjs +75 -10
  151. package/v4/mini/schemas.d.cts +49 -1
  152. package/v4/mini/schemas.d.ts +49 -1
  153. package/v4/mini/schemas.js +49 -2
  154. package/src/v4/core/function.ts +0 -176
  155. package/v4/core/function.cjs +0 -102
  156. package/v4/core/function.d.cts +0 -52
  157. package/v4/core/function.d.ts +0 -52
  158. package/v4/core/function.js +0 -75
@@ -0,0 +1,532 @@
1
+ import { expect, expectTypeOf, test } from "vitest";
2
+ import * as z from "zod/v4";
3
+
4
+ const isoDateCodec = z.codec(
5
+ z.iso.datetime(), // Input: ISO string (validates to string)
6
+ z.date(), // Output: Date object
7
+ {
8
+ decode: (isoString) => new Date(isoString), // Forward: ISO string → Date
9
+ encode: (date) => date.toISOString(), // Backward: Date → ISO string
10
+ }
11
+ );
12
+
13
+ test("instanceof", () => {
14
+ expect(isoDateCodec instanceof z.ZodCodec).toBe(true);
15
+ expect(isoDateCodec instanceof z.ZodPipe).toBe(true);
16
+ expect(isoDateCodec instanceof z.ZodType).toBe(true);
17
+ expect(isoDateCodec instanceof z.core.$ZodCodec).toBe(true);
18
+ expect(isoDateCodec instanceof z.core.$ZodPipe).toBe(true);
19
+ expect(isoDateCodec instanceof z.core.$ZodType).toBe(true);
20
+
21
+ expectTypeOf(isoDateCodec.def).toEqualTypeOf<z.core.$ZodCodecDef<z.ZodISODateTime, z.ZodDate>>();
22
+ });
23
+
24
+ test("codec basic functionality", () => {
25
+ // ISO string -> Date codec using z.iso.datetime() for input validation
26
+
27
+ const testIsoString = "2024-01-15T10:30:00.000Z";
28
+ const testDate = new Date("2024-01-15T10:30:00.000Z");
29
+
30
+ // Forward decoding (ISO string -> Date)
31
+ const decodedResult = z.decode(isoDateCodec, testIsoString);
32
+ expect(decodedResult).toBeInstanceOf(Date);
33
+ expect(decodedResult.toISOString()).toMatchInlineSnapshot(`"2024-01-15T10:30:00.000Z"`);
34
+
35
+ // Backward encoding (Date -> ISO string)
36
+ const encodedResult = z.encode(isoDateCodec, testDate);
37
+ expect(typeof encodedResult).toBe("string");
38
+ expect(encodedResult).toMatchInlineSnapshot(`"2024-01-15T10:30:00.000Z"`);
39
+ });
40
+
41
+ test("codec round trip", () => {
42
+ const isoDateCodec = z.codec(z.iso.datetime(), z.date(), {
43
+ decode: (isoString) => new Date(isoString),
44
+ encode: (date) => date.toISOString(),
45
+ });
46
+
47
+ const original = "2024-12-25T15:45:30.123Z";
48
+ const toDate = z.decode(isoDateCodec, original);
49
+ const backToString = z.encode(isoDateCodec, toDate);
50
+
51
+ expect(backToString).toMatchInlineSnapshot(`"2024-12-25T15:45:30.123Z"`);
52
+ expect(toDate).toBeInstanceOf(Date);
53
+ expect(toDate.getTime()).toMatchInlineSnapshot(`1735141530123`);
54
+ });
55
+
56
+ test("codec with refinement", () => {
57
+ const isoDateCodec = z
58
+ .codec(z.iso.datetime(), z.date(), {
59
+ decode: (isoString) => new Date(isoString),
60
+ encode: (date) => date.toISOString(),
61
+ })
62
+ .refine((val) => val.getFullYear() === 2024, { error: "Year must be 2024" });
63
+
64
+ // Valid 2024 date
65
+ const validDate = z.decode(isoDateCodec, "2024-01-15T10:30:00.000Z");
66
+ expect(validDate.getFullYear()).toMatchInlineSnapshot(`2024`);
67
+ expect(validDate.getTime()).toMatchInlineSnapshot(`1705314600000`);
68
+
69
+ // Invalid year should fail safely
70
+ const invalidYearResult = z.safeDecode(isoDateCodec, "2023-01-15T10:30:00.000Z");
71
+ expect(invalidYearResult.success).toBe(false);
72
+ if (!invalidYearResult.success) {
73
+ expect(invalidYearResult.error.issues).toMatchInlineSnapshot(`
74
+ [
75
+ {
76
+ "code": "custom",
77
+ "message": "Year must be 2024",
78
+ "path": [],
79
+ },
80
+ ]
81
+ `);
82
+ }
83
+ });
84
+
85
+ test("safe codec operations", () => {
86
+ const isoDateCodec = z.codec(z.iso.datetime(), z.date(), {
87
+ decode: (isoString) => new Date(isoString),
88
+ encode: (date) => date.toISOString(),
89
+ });
90
+
91
+ // Safe decode with invalid input
92
+ const safeDecodeResult = z.safeDecode(isoDateCodec, "invalid-date");
93
+ expect(safeDecodeResult.success).toBe(false);
94
+ if (!safeDecodeResult.success) {
95
+ expect(safeDecodeResult.error.issues).toMatchInlineSnapshot(`
96
+ [
97
+ {
98
+ "code": "invalid_format",
99
+ "format": "datetime",
100
+ "message": "Invalid ISO datetime",
101
+ "origin": "string",
102
+ "path": [],
103
+ "pattern": "/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/",
104
+ },
105
+ ]
106
+ `);
107
+ }
108
+
109
+ // Safe decode with valid input
110
+ const safeDecodeValid = z.safeDecode(isoDateCodec, "2024-01-15T10:30:00.000Z");
111
+ expect(safeDecodeValid.success).toBe(true);
112
+ if (safeDecodeValid.success) {
113
+ expect(safeDecodeValid.data).toBeInstanceOf(Date);
114
+ expect(safeDecodeValid.data.getTime()).toMatchInlineSnapshot(`1705314600000`);
115
+ }
116
+
117
+ // Safe encode with valid input
118
+ const safeEncodeResult = z.safeEncode(isoDateCodec, new Date("2024-01-01"));
119
+ expect(safeEncodeResult.success).toBe(true);
120
+ if (safeEncodeResult.success) {
121
+ expect(safeEncodeResult.data).toMatchInlineSnapshot(`"2024-01-01T00:00:00.000Z"`);
122
+ }
123
+ });
124
+
125
+ test("codec with different types", () => {
126
+ // String -> Number codec
127
+ const stringNumberCodec = z.codec(z.string(), z.number(), {
128
+ decode: (str) => Number.parseFloat(str),
129
+ encode: (num) => num.toString(),
130
+ });
131
+
132
+ const decodedNumber = z.decode(stringNumberCodec, "42.5");
133
+ expect(decodedNumber).toMatchInlineSnapshot(`42.5`);
134
+ expect(typeof decodedNumber).toBe("number");
135
+
136
+ const encodedString = z.encode(stringNumberCodec, 42.5);
137
+ expect(encodedString).toMatchInlineSnapshot(`"42.5"`);
138
+ expect(typeof encodedString).toBe("string");
139
+ });
140
+
141
+ test("async codec operations", async () => {
142
+ const isoDateCodec = z.codec(z.iso.datetime(), z.date(), {
143
+ decode: (isoString) => new Date(isoString),
144
+ encode: (date) => date.toISOString(),
145
+ });
146
+
147
+ // Async decode
148
+ const decodedResult = await z.decodeAsync(isoDateCodec, "2024-01-15T10:30:00.000Z");
149
+ expect(decodedResult).toBeInstanceOf(Date);
150
+ expect(decodedResult.getTime()).toMatchInlineSnapshot(`1705314600000`);
151
+
152
+ // Async encode
153
+ const encodedResult = await z.encodeAsync(isoDateCodec, new Date("2024-01-15T10:30:00.000Z"));
154
+ expect(typeof encodedResult).toBe("string");
155
+ expect(encodedResult).toMatchInlineSnapshot(`"2024-01-15T10:30:00.000Z"`);
156
+
157
+ // Safe async operations
158
+ const safeDecodeResult = await z.safeDecodeAsync(isoDateCodec, "2024-01-15T10:30:00.000Z");
159
+ expect(safeDecodeResult.success).toBe(true);
160
+ if (safeDecodeResult.success) {
161
+ expect(safeDecodeResult.data.getTime()).toMatchInlineSnapshot(`1705314600000`);
162
+ }
163
+
164
+ const safeEncodeResult = await z.safeEncodeAsync(isoDateCodec, new Date("2024-01-15T10:30:00.000Z"));
165
+ expect(safeEncodeResult.success).toBe(true);
166
+ if (safeEncodeResult.success) {
167
+ expect(safeEncodeResult.data).toMatchInlineSnapshot(`"2024-01-15T10:30:00.000Z"`);
168
+ }
169
+ });
170
+
171
+ test("codec type inference", () => {
172
+ const codec = z.codec(z.string(), z.number(), {
173
+ decode: (str) => Number.parseInt(str),
174
+ encode: (num) => num.toString(),
175
+ });
176
+
177
+ // These should compile without type errors
178
+ const decoded: number = z.decode(codec, "123");
179
+ const encoded: string = z.encode(codec, 123);
180
+
181
+ expect(decoded).toMatchInlineSnapshot(`123`);
182
+ expect(encoded).toMatchInlineSnapshot(`"123"`);
183
+ });
184
+
185
+ test("nested codec with object containing codec property", () => {
186
+ // Nested schema: object containing a codec as one of its properties, with refinements at all levels
187
+ const waypointSchema = z
188
+ .object({
189
+ name: z.string().min(1, "Waypoint name required"),
190
+ difficulty: z.enum(["easy", "medium", "hard"]),
191
+ coordinate: z
192
+ .codec(
193
+ z
194
+ .string()
195
+ .regex(/^-?\d+,-?\d+$/, "Must be 'x,y' format"), // Input: coordinate string
196
+ z
197
+ .object({ x: z.number(), y: z.number() })
198
+ .refine((coord) => coord.x >= 0 && coord.y >= 0, { error: "Coordinates must be non-negative" }), // Output: coordinate object
199
+ {
200
+ decode: (coordString: string) => {
201
+ const [x, y] = coordString.split(",").map(Number);
202
+ return { x, y };
203
+ },
204
+ encode: (coord: { x: number; y: number }) => `${coord.x},${coord.y}`,
205
+ }
206
+ )
207
+ .refine((coord) => coord.x <= 1000 && coord.y <= 1000, { error: "Coordinates must be within bounds" }),
208
+ })
209
+ .refine((waypoint) => waypoint.difficulty !== "hard" || waypoint.coordinate.x >= 100, {
210
+ error: "Hard waypoints must be at least 100 units from origin",
211
+ });
212
+
213
+ // Test data
214
+ const inputWaypoint = {
215
+ name: "Summit Point",
216
+ difficulty: "medium" as const,
217
+ coordinate: "150,200",
218
+ };
219
+
220
+ // Forward decoding (object with string coordinate -> object with coordinate object)
221
+ const decodedWaypoint = z.decode(waypointSchema, inputWaypoint);
222
+ expect(decodedWaypoint).toMatchInlineSnapshot(`
223
+ {
224
+ "coordinate": {
225
+ "x": 150,
226
+ "y": 200,
227
+ },
228
+ "difficulty": "medium",
229
+ "name": "Summit Point",
230
+ }
231
+ `);
232
+
233
+ // Backward encoding (object with coordinate object -> object with string coordinate)
234
+ const encodedWaypoint = z.encode(waypointSchema, decodedWaypoint);
235
+ expect(encodedWaypoint).toMatchInlineSnapshot(`
236
+ {
237
+ "coordinate": "150,200",
238
+ "difficulty": "medium",
239
+ "name": "Summit Point",
240
+ }
241
+ `);
242
+
243
+ // Test refinements at all levels
244
+ // String validation (empty waypoint name)
245
+ const emptyNameResult = z.safeDecode(waypointSchema, {
246
+ name: "",
247
+ difficulty: "easy",
248
+ coordinate: "10,20",
249
+ });
250
+ expect(emptyNameResult.success).toBe(false);
251
+ if (!emptyNameResult.success) {
252
+ expect(emptyNameResult.error.issues).toMatchInlineSnapshot(`
253
+ [
254
+ {
255
+ "code": "too_small",
256
+ "inclusive": true,
257
+ "message": "Waypoint name required",
258
+ "minimum": 1,
259
+ "origin": "string",
260
+ "path": [
261
+ "name",
262
+ ],
263
+ },
264
+ ]
265
+ `);
266
+ }
267
+
268
+ // Enum validation (invalid difficulty)
269
+ const invalidDifficultyResult = z.safeDecode(waypointSchema, {
270
+ name: "Test Point",
271
+ difficulty: "impossible" as any,
272
+ coordinate: "10,20",
273
+ });
274
+ expect(invalidDifficultyResult.success).toBe(false);
275
+ if (!invalidDifficultyResult.success) {
276
+ expect(invalidDifficultyResult.error.issues).toMatchInlineSnapshot(`
277
+ [
278
+ {
279
+ "code": "invalid_value",
280
+ "message": "Invalid option: expected one of "easy"|"medium"|"hard"",
281
+ "path": [
282
+ "difficulty",
283
+ ],
284
+ "values": [
285
+ "easy",
286
+ "medium",
287
+ "hard",
288
+ ],
289
+ },
290
+ ]
291
+ `);
292
+ }
293
+
294
+ // Codec string format validation (invalid coordinate format)
295
+ const invalidFormatResult = z.safeDecode(waypointSchema, {
296
+ name: "Test Point",
297
+ difficulty: "easy",
298
+ coordinate: "invalid",
299
+ });
300
+ expect(invalidFormatResult.success).toBe(false);
301
+ if (!invalidFormatResult.success) {
302
+ expect(invalidFormatResult.error.issues).toMatchInlineSnapshot(`
303
+ [
304
+ {
305
+ "code": "invalid_format",
306
+ "format": "regex",
307
+ "message": "Must be 'x,y' format",
308
+ "origin": "string",
309
+ "path": [
310
+ "coordinate",
311
+ ],
312
+ "pattern": "/^-?\\d+,-?\\d+$/",
313
+ },
314
+ ]
315
+ `);
316
+ }
317
+
318
+ // Codec object refinement (negative coordinates)
319
+ const negativeCoordResult = z.safeDecode(waypointSchema, {
320
+ name: "Test Point",
321
+ difficulty: "easy",
322
+ coordinate: "-5,10",
323
+ });
324
+ expect(negativeCoordResult.success).toBe(false);
325
+ if (!negativeCoordResult.success) {
326
+ expect(negativeCoordResult.error.issues).toMatchInlineSnapshot(`
327
+ [
328
+ {
329
+ "code": "custom",
330
+ "message": "Coordinates must be non-negative",
331
+ "path": [
332
+ "coordinate",
333
+ ],
334
+ },
335
+ ]
336
+ `);
337
+ }
338
+
339
+ // Codec-level refinement (coordinates out of bounds)
340
+ const outOfBoundsResult = z.safeDecode(waypointSchema, {
341
+ name: "Test Point",
342
+ difficulty: "easy",
343
+ coordinate: "1500,2000",
344
+ });
345
+ expect(outOfBoundsResult.success).toBe(false);
346
+ if (!outOfBoundsResult.success) {
347
+ expect(outOfBoundsResult.error.issues).toMatchInlineSnapshot(`
348
+ [
349
+ {
350
+ "code": "custom",
351
+ "message": "Coordinates must be within bounds",
352
+ "path": [
353
+ "coordinate",
354
+ ],
355
+ },
356
+ ]
357
+ `);
358
+ }
359
+
360
+ // Object-level refinement (hard waypoint too close to origin)
361
+ const hardWaypointResult = z.safeDecode(waypointSchema, {
362
+ name: "Expert Point",
363
+ difficulty: "hard",
364
+ coordinate: "50,60", // x < 100, but hard waypoints need x >= 100
365
+ });
366
+ expect(hardWaypointResult.success).toBe(false);
367
+ if (!hardWaypointResult.success) {
368
+ expect(hardWaypointResult.error.issues).toMatchInlineSnapshot(`
369
+ [
370
+ {
371
+ "code": "custom",
372
+ "message": "Hard waypoints must be at least 100 units from origin",
373
+ "path": [],
374
+ },
375
+ ]
376
+ `);
377
+ }
378
+
379
+ // Round trip test
380
+ const roundTripResult = z.encode(waypointSchema, z.decode(waypointSchema, inputWaypoint));
381
+ expect(roundTripResult).toMatchInlineSnapshot(`
382
+ {
383
+ "coordinate": "150,200",
384
+ "difficulty": "medium",
385
+ "name": "Summit Point",
386
+ }
387
+ `);
388
+ });
389
+
390
+ test("mutating refinements", () => {
391
+ const A = z.codec(z.string(), z.string().trim(), {
392
+ decode: (val) => val,
393
+ encode: (val) => val,
394
+ });
395
+
396
+ expect(z.decode(A, " asdf ")).toMatchInlineSnapshot(`"asdf"`);
397
+ expect(z.encode(A, " asdf ")).toMatchInlineSnapshot(`"asdf"`);
398
+
399
+ const B = z
400
+ .codec(z.string(), z.string(), {
401
+ decode: (val) => val,
402
+ encode: (val) => val,
403
+ })
404
+ .check(z.trim(), z.maxLength(4));
405
+
406
+ expect(z.decode(B, " asdf ")).toMatchInlineSnapshot(`"asdf"`);
407
+ expect(z.encode(B, " asdf ")).toMatchInlineSnapshot(`"asdf"`);
408
+ });
409
+
410
+ test("codec type enforcement - correct encode/decode signatures", () => {
411
+ // Test that codec functions have correct type signatures
412
+ const stringToNumberCodec = z.codec(z.string(), z.number(), {
413
+ decode: (value: string) => Number(value), // core.output<A> -> core.input<B>
414
+ encode: (value: number) => String(value), // core.input<B> -> core.output<A>
415
+ });
416
+
417
+ // These should compile without errors - correct types
418
+ expectTypeOf<(value: string, payload: z.core.ParsePayload<string>) => number>(
419
+ stringToNumberCodec.def.transform
420
+ ).toBeFunction();
421
+ expectTypeOf<(value: number, payload: z.core.ParsePayload<number>) => string>(
422
+ stringToNumberCodec.def.reverseTransform
423
+ ).toBeFunction();
424
+
425
+ // Test that decode parameter type is core.output<A> (string)
426
+ const validDecode = (value: string) => Number(value);
427
+ expectTypeOf(validDecode).toMatchTypeOf<(value: string) => number>();
428
+
429
+ // Test that encode parameter type is core.input<B> (number)
430
+ const validEncode = (value: number) => String(value);
431
+ expectTypeOf(validEncode).toMatchTypeOf<(value: number) => string>();
432
+
433
+ z.codec(z.string(), z.number(), {
434
+ // @ts-expect-error - decode should NOT accept core.input<A> as parameter
435
+ decode: (value: never, _payload) => Number(value), // Wrong: should be string, not unknown
436
+ encode: (value: number, _payload) => String(value),
437
+ });
438
+
439
+ z.codec(z.string(), z.number(), {
440
+ decode: (value: string) => Number(value),
441
+ // @ts-expect-error - encode should NOT accept core.output<B> as parameter
442
+ encode: (value: never) => String(value), // Wrong: should be number, not unknown
443
+ });
444
+
445
+ z.codec(z.string(), z.number(), {
446
+ // @ts-expect-error - decode return type should be core.input<B>
447
+ decode: (value: string) => String(value), // Wrong: should return number, not string
448
+ encode: (value: number) => String(value),
449
+ });
450
+
451
+ z.codec(z.string(), z.number(), {
452
+ decode: (value: string) => Number(value),
453
+ // @ts-expect-error - encode return type should be core.output<A>
454
+ encode: (value: number) => Number(value), // Wrong: should return string, not number
455
+ });
456
+ });
457
+
458
+ test("codec type enforcement - complex types", () => {
459
+ type User = { id: number; name: string };
460
+ type UserInput = { id: string; name: string };
461
+
462
+ const userCodec = z.codec(
463
+ z.object({ id: z.string(), name: z.string() }),
464
+ z.object({ id: z.number(), name: z.string() }),
465
+ {
466
+ decode: (input: UserInput) => ({ id: Number(input.id), name: input.name }),
467
+ encode: (user: User) => ({ id: String(user.id), name: user.name }),
468
+ }
469
+ );
470
+
471
+ // Verify correct types are inferred
472
+ expectTypeOf<(input: UserInput, payload: z.core.ParsePayload<UserInput>) => User>(
473
+ userCodec.def.transform
474
+ ).toBeFunction();
475
+ expectTypeOf<(user: User, payload: z.core.ParsePayload<User>) => UserInput>(
476
+ userCodec.def.reverseTransform
477
+ ).toBeFunction();
478
+
479
+ z.codec(
480
+ z.object({
481
+ id: z.string(),
482
+ name: z.string(),
483
+ }),
484
+ z.object({ id: z.number(), name: z.string() }),
485
+ {
486
+ // @ts-expect-error - decode parameter should be UserInput, not User
487
+ decode: (input: User) => ({ id: Number(input.id), name: input.name }), // Wrong type
488
+ encode: (user: User) => ({ id: String(user.id), name: user.name }),
489
+ }
490
+ );
491
+
492
+ z.codec(
493
+ z.object({
494
+ id: z.string(),
495
+ name: z.string(),
496
+ }),
497
+ z.object({ id: z.number(), name: z.string() }),
498
+ {
499
+ decode: (input: UserInput) => ({ id: Number(input.id), name: input.name }),
500
+ // @ts-expect-error - encode parameter should be User, not UserInput
501
+ encode: (user: UserInput) => ({ id: String(user.id), name: user.name }), // Wrong type
502
+ }
503
+ );
504
+ });
505
+
506
+ test("codex with overwrites", () => {
507
+ const stringPlusA = z.string().overwrite((val) => val + "a");
508
+ const A = z
509
+ .codec(stringPlusA, stringPlusA, {
510
+ decode: (val) => val,
511
+ encode: (val) => val,
512
+ })
513
+ .overwrite((val) => val + "a");
514
+
515
+ expect(z.decode(A, "")).toEqual("aaa");
516
+ expect(z.encode(A, "")).toEqual("aaa");
517
+
518
+ // @ts-expect-error
519
+ expect(z.safeEncode(A, Symbol("a"))).toMatchInlineSnapshot(`
520
+ {
521
+ "error": [ZodError: [
522
+ {
523
+ "expected": "string",
524
+ "code": "invalid_type",
525
+ "path": [],
526
+ "message": "Invalid input: expected string, received symbol"
527
+ }
528
+ ]],
529
+ "success": false,
530
+ }
531
+ `);
532
+ });
@@ -62,7 +62,7 @@ test("continuability", () => {
62
62
  "message": "Invalid UUID",
63
63
  "origin": "string",
64
64
  "path": [],
65
- "pattern": "/^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000)$/",
65
+ "pattern": "/^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/",
66
66
  },
67
67
  {
68
68
  "code": "custom",
@@ -323,3 +323,35 @@ test("defaulted object schema returns shallow clone", () => {
323
323
  expect(result1).not.toBe(result2);
324
324
  expect(result1).toEqual(result2);
325
325
  });
326
+
327
+ test("direction-aware defaults", () => {
328
+ const schema = z.string().default("hello");
329
+
330
+ // Forward direction (regular parse): defaults should be applied
331
+ expect(schema.parse(undefined)).toBe("hello");
332
+ expect(schema.parse("hello")).toBe("hello");
333
+
334
+ // Reverse direction (encode): defaults should NOT be applied, undefined should fail validation
335
+ expect(() => z.encode(schema, undefined as any)).toThrow();
336
+
337
+ // But valid values should still work in reverse
338
+ expect(z.safeEncode(schema, "world")).toMatchInlineSnapshot(`
339
+ {
340
+ "data": "world",
341
+ "success": true,
342
+ }
343
+ `);
344
+ expect(z.safeEncode(schema, undefined as any)).toMatchInlineSnapshot(`
345
+ {
346
+ "error": [ZodError: [
347
+ {
348
+ "expected": "string",
349
+ "code": "invalid_type",
350
+ "path": [],
351
+ "message": "Invalid input: expected string, received undefined"
352
+ }
353
+ ]],
354
+ "success": false,
355
+ }
356
+ `);
357
+ });
@@ -82,6 +82,8 @@ test("first party switch", () => {
82
82
  break;
83
83
  case "lazy":
84
84
  break;
85
+ case "function":
86
+ break;
85
87
  default:
86
88
  expectTypeOf(def).toEqualTypeOf<never>();
87
89
  }
@@ -168,6 +170,8 @@ test("$ZodSchemaTypes", () => {
168
170
  break;
169
171
  case "lazy":
170
172
  break;
173
+ case "function":
174
+ break;
171
175
 
172
176
  default:
173
177
  expectTypeOf(type).toEqualTypeOf<never>();
@@ -32,38 +32,38 @@ test("function inference 1", () => {
32
32
  expectTypeOf<func1>().toEqualTypeOf<(k: string) => number>();
33
33
  });
34
34
 
35
- // test("method parsing", () => {
36
- // const methodObject = z.object({
37
- // property: z.number(),
38
- // method: z
39
- // .function()
40
- // .input(z.tuple([z.string()]))
41
- // .output(z.number()),
42
- // });
43
- // const methodInstance = {
44
- // property: 3,
45
- // method: function (s: string) {
46
- // return s.length + this.property;
47
- // },
48
- // };
49
- // const parsed = methodObject.parse(methodInstance);
50
- // expect(parsed.method("length=8")).toBe(11); // 8 length + 3 property
51
- // });
35
+ test("method parsing", () => {
36
+ const methodObject = z.object({
37
+ property: z.number(),
38
+ method: z
39
+ .function()
40
+ .input(z.tuple([z.string()]))
41
+ .output(z.number()),
42
+ });
43
+ const methodInstance = {
44
+ property: 3,
45
+ method: function (s: string) {
46
+ return s.length + this.property;
47
+ },
48
+ };
49
+ const parsed = methodObject.parse(methodInstance);
50
+ expect(parsed.method("length=8")).toBe(11); // 8 length + 3 property
51
+ });
52
52
 
53
- // test("async method parsing", async () => {
54
- // const methodObject = z.object({
55
- // property: z.number(),
56
- // method: z.function().input(z.string()).output(z.promise(z.number())),
57
- // });
58
- // const methodInstance = {
59
- // property: 3,
60
- // method: async function (s: string) {
61
- // return s.length + this.property;
62
- // },
63
- // };
64
- // const parsed = methodObject.parse(methodInstance);
65
- // expect(await parsed.method("length=8")).toBe(11); // 8 length + 3 property
66
- // });
53
+ test("async method parsing", async () => {
54
+ const methodObject = z.object({
55
+ property: z.number(),
56
+ method: z.function().input([z.string()]).output(z.promise(z.number())),
57
+ });
58
+ const methodInstance = {
59
+ property: 3,
60
+ method: async function (s: string) {
61
+ return s.length + this.property;
62
+ },
63
+ };
64
+ const parsed = methodObject.parse(methodInstance);
65
+ expect(await parsed.method("length=8")).toBe(11); // 8 length + 3 property
66
+ });
67
67
 
68
68
  test("args method", () => {
69
69
  const t1 = z.function();