zod 4.1.0-canary.20250723T222937 → 4.1.0-canary.20250724T211341

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 (58) hide show
  1. package/package.json +1 -1
  2. package/src/v3/types.ts +3 -1
  3. package/src/v4/classic/errors.ts +9 -2
  4. package/src/v4/classic/schemas.ts +10 -7
  5. package/src/v4/classic/tests/error-utils.test.ts +43 -0
  6. package/src/v4/classic/tests/file.test.ts +0 -1
  7. package/src/v4/classic/tests/partial.test.ts +193 -0
  8. package/src/v4/classic/tests/pickomit.test.ts +5 -5
  9. package/src/v4/classic/tests/preprocess.test.ts +4 -15
  10. package/src/v4/classic/tests/record.test.ts +15 -1
  11. package/src/v4/classic/tests/recursive-types.test.ts +67 -0
  12. package/src/v4/classic/tests/string.test.ts +77 -0
  13. package/src/v4/classic/tests/to-json-schema.test.ts +1 -0
  14. package/src/v4/classic/tests/transform.test.ts +104 -0
  15. package/src/v4/classic/tests/union.test.ts +90 -3
  16. package/src/v4/core/checks.ts +2 -2
  17. package/src/v4/core/errors.ts +8 -15
  18. package/src/v4/core/registries.ts +3 -2
  19. package/src/v4/core/schemas.ts +92 -94
  20. package/src/v4/core/tests/extend.test.ts +18 -0
  21. package/src/v4/core/to-json-schema.ts +1 -0
  22. package/src/v4/core/util.ts +135 -98
  23. package/src/v4/core/versions.ts +1 -1
  24. package/src/v4/mini/schemas.ts +3 -1
  25. package/v3/types.cjs +2 -0
  26. package/v3/types.d.cts +4 -1
  27. package/v3/types.d.ts +4 -1
  28. package/v3/types.js +2 -0
  29. package/v4/classic/errors.cjs +9 -2
  30. package/v4/classic/errors.js +9 -2
  31. package/v4/classic/schemas.cjs +5 -3
  32. package/v4/classic/schemas.d.cts +2 -1
  33. package/v4/classic/schemas.d.ts +2 -1
  34. package/v4/classic/schemas.js +5 -3
  35. package/v4/core/checks.d.cts +2 -2
  36. package/v4/core/checks.d.ts +2 -2
  37. package/v4/core/errors.cjs +4 -9
  38. package/v4/core/errors.d.cts +4 -6
  39. package/v4/core/errors.d.ts +4 -6
  40. package/v4/core/errors.js +4 -9
  41. package/v4/core/registries.cjs +2 -1
  42. package/v4/core/registries.d.cts +1 -1
  43. package/v4/core/registries.d.ts +1 -1
  44. package/v4/core/registries.js +2 -1
  45. package/v4/core/schemas.cjs +50 -87
  46. package/v4/core/schemas.d.cts +8 -3
  47. package/v4/core/schemas.d.ts +8 -3
  48. package/v4/core/schemas.js +50 -87
  49. package/v4/core/to-json-schema.cjs +1 -0
  50. package/v4/core/to-json-schema.js +1 -0
  51. package/v4/core/util.cjs +123 -97
  52. package/v4/core/util.d.cts +2 -0
  53. package/v4/core/util.d.ts +2 -0
  54. package/v4/core/util.js +121 -97
  55. package/v4/core/versions.cjs +1 -1
  56. package/v4/core/versions.js +1 -1
  57. package/v4/mini/schemas.cjs +3 -1
  58. package/v4/mini/schemas.js +3 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zod",
3
- "version": "4.1.0-canary.20250723T222937",
3
+ "version": "4.1.0-canary.20250724T211341",
4
4
  "type": "module",
5
5
  "author": "Colin McDonnell <zod@colinhacks.com>",
6
6
  "description": "TypeScript-first schema declaration and validation library with static type inference",
package/src/v3/types.ts CHANGED
@@ -705,6 +705,7 @@ function isValidJWT(jwt: string, alg?: string): boolean {
705
705
  .replace(/-/g, "+")
706
706
  .replace(/_/g, "/")
707
707
  .padEnd(header.length + ((4 - (header.length % 4)) % 4), "=");
708
+ // @ts-ignore
708
709
  const decoded = JSON.parse(atob(base64));
709
710
  if (typeof decoded !== "object" || decoded === null) return false;
710
711
  if ("typ" in decoded && decoded?.typ !== "JWT") return false;
@@ -875,6 +876,7 @@ export class ZodString extends ZodType<string, ZodStringDef, string> {
875
876
  }
876
877
  } else if (check.kind === "url") {
877
878
  try {
879
+ // @ts-ignore
878
880
  new URL(input.data);
879
881
  } catch {
880
882
  ctx = this._getOrReturnCtx(input, ctx);
@@ -2454,7 +2456,7 @@ export class ZodObject<
2454
2456
  Output = objectOutputType<T, Catchall, UnknownKeys>,
2455
2457
  Input = objectInputType<T, Catchall, UnknownKeys>,
2456
2458
  > extends ZodType<Output, ZodObjectDef<T, UnknownKeys, Catchall>, Input> {
2457
- private _cached: { shape: T; keys: string[] } | null = null;
2459
+ _cached: { shape: T; keys: string[] } | null = null;
2458
2460
 
2459
2461
  _getCached(): { shape: T; keys: string[] } {
2460
2462
  if (this._cached !== null) return this._cached;
@@ -1,5 +1,6 @@
1
1
  import * as core from "../core/index.js";
2
2
  import { $ZodError } from "../core/index.js";
3
+ import * as util from "../core/util.js";
3
4
 
4
5
  /** @deprecated Use `z.core.$ZodIssue` from `@zod/core` instead, especially if you are building a library on top of Zod. */
5
6
  export type ZodIssue = core.$ZodIssue;
@@ -34,11 +35,17 @@ const initializer = (inst: ZodError, issues: core.$ZodIssue[]) => {
34
35
  // enumerable: false,
35
36
  },
36
37
  addIssue: {
37
- value: (issue: any) => inst.issues.push(issue),
38
+ value: (issue: any) => {
39
+ inst.issues.push(issue);
40
+ inst.message = JSON.stringify(inst.issues, util.jsonStringifyReplacer, 2);
41
+ },
38
42
  // enumerable: false,
39
43
  },
40
44
  addIssues: {
41
- value: (issues: any) => inst.issues.push(...issues),
45
+ value: (issues: any) => {
46
+ inst.issues.push(...issues);
47
+ inst.message = JSON.stringify(inst.issues, util.jsonStringifyReplacer, 2);
48
+ },
42
49
  // enumerable: false,
43
50
  },
44
51
  isEmpty: {
@@ -1068,9 +1068,7 @@ export interface ZodObject<
1068
1068
  /** This is the default behavior. This method call is likely unnecessary. */
1069
1069
  strip(): ZodObject<Shape, core.$strip>;
1070
1070
 
1071
- extend<U extends core.$ZodLooseShape & Partial<Record<keyof Shape, core.SomeType>>>(
1072
- shape: U
1073
- ): ZodObject<util.Extend<Shape, U>, Config>;
1071
+ extend<U extends core.$ZodLooseShape>(shape: U): ZodObject<util.Extend<Shape, U>, Config>;
1074
1072
 
1075
1073
  /**
1076
1074
  * @deprecated Use [`A.extend(B.shape)`](https://zod.dev/api?id=extend) instead.
@@ -1130,7 +1128,6 @@ export const ZodObject: core.$constructor<ZodObject> = /*@__PURE__*/ core.$const
1130
1128
  inst.keyof = () => _enum(Object.keys(inst._zod.def.shape)) as any;
1131
1129
  inst.catchall = (catchall) => inst.clone({ ...inst._zod.def, catchall: catchall as any as core.$ZodType }) as any;
1132
1130
  inst.passthrough = () => inst.clone({ ...inst._zod.def, catchall: unknown() });
1133
- // inst.nonstrict = () => inst.clone({ ...inst._zod.def, catchall: api.unknown() });
1134
1131
  inst.loose = () => inst.clone({ ...inst._zod.def, catchall: unknown() });
1135
1132
  inst.strict = () => inst.clone({ ...inst._zod.def, catchall: never() });
1136
1133
  inst.strip = () => inst.clone({ ...inst._zod.def, catchall: undefined });
@@ -1350,9 +1347,11 @@ export function partialRecord<Key extends core.$ZodRecordKey, Value extends core
1350
1347
  valueType: Value,
1351
1348
  params?: string | core.$ZodRecordParams
1352
1349
  ): ZodRecord<Key & core.$partial, Value> {
1350
+ const k = core.clone(keyType);
1351
+ k._zod.values = undefined;
1353
1352
  return new ZodRecord({
1354
1353
  type: "record",
1355
- keyType: union([keyType, never()]),
1354
+ keyType: k,
1356
1355
  valueType: valueType as any,
1357
1356
  ...util.normalizeParams(params),
1358
1357
  }) as any;
@@ -1584,7 +1583,7 @@ export const ZodTransform: core.$constructor<ZodTransform> = /*@__PURE__*/ core.
1584
1583
  _issue.code ??= "custom";
1585
1584
  _issue.input ??= payload.value;
1586
1585
  _issue.inst ??= inst;
1587
- _issue.continue ??= true;
1586
+ // _issue.continue ??= true;
1588
1587
  payload.issues.push(util.issue(_issue));
1589
1588
  }
1590
1589
  };
@@ -1838,12 +1837,16 @@ export function pipe(in_: core.SomeType, out: core.SomeType) {
1838
1837
  // ZodReadonly
1839
1838
  export interface ZodReadonly<T extends core.SomeType = core.$ZodType>
1840
1839
  extends _ZodType<core.$ZodReadonlyInternals<T>>,
1841
- core.$ZodReadonly<T> {}
1840
+ core.$ZodReadonly<T> {
1841
+ unwrap(): T;
1842
+ }
1842
1843
  export const ZodReadonly: core.$constructor<ZodReadonly> = /*@__PURE__*/ core.$constructor(
1843
1844
  "ZodReadonly",
1844
1845
  (inst, def) => {
1845
1846
  core.$ZodReadonly.init(inst, def);
1846
1847
  ZodType.init(inst, def);
1848
+
1849
+ inst.unwrap = () => inst._zod.def.innerType;
1847
1850
  }
1848
1851
  );
1849
1852
 
@@ -550,3 +550,46 @@ test("disc union treeify/format", () => {
550
550
  }
551
551
  `);
552
552
  });
553
+
554
+ test("update message after adding issues", () => {
555
+ const e = new z.ZodError([]);
556
+ e.addIssue({
557
+ code: "custom",
558
+ message: "message",
559
+ input: "asdf",
560
+ path: [],
561
+ });
562
+ expect(e.message).toMatchInlineSnapshot(`
563
+ "[
564
+ {
565
+ "code": "custom",
566
+ "message": "message",
567
+ "input": "asdf",
568
+ "path": []
569
+ }
570
+ ]"
571
+ `);
572
+
573
+ e.addIssue({
574
+ code: "custom",
575
+ message: "message",
576
+ input: "asdf",
577
+ path: [],
578
+ });
579
+ expect(e.message).toMatchInlineSnapshot(`
580
+ "[
581
+ {
582
+ "code": "custom",
583
+ "message": "message",
584
+ "input": "asdf",
585
+ "path": []
586
+ },
587
+ {
588
+ "code": "custom",
589
+ "message": "message",
590
+ "input": "asdf",
591
+ "path": []
592
+ }
593
+ ]"
594
+ `);
595
+ });
@@ -1,4 +1,3 @@
1
- // @ts-ignore
2
1
  import { File as WebFile } from "@web-std/file";
3
2
 
4
3
  import { afterEach, beforeEach, expect, test } from "vitest";
@@ -145,3 +145,196 @@ test("partial with mask -- ignore falsy values", async () => {
145
145
  masked.parse({ country: "US" });
146
146
  await masked.parseAsync({ country: "US" });
147
147
  });
148
+
149
+ test("catch/prefault/default", () => {
150
+ const mySchema = z.object({
151
+ a: z.string().catch("catch value").optional(),
152
+ b: z.string().default("default value").optional(),
153
+ c: z.string().prefault("prefault value").optional(),
154
+ d: z.string().catch("catch value"),
155
+ e: z.string().default("default value"),
156
+ f: z.string().prefault("prefault value"),
157
+ });
158
+
159
+ expect(mySchema.parse({})).toMatchInlineSnapshot(`
160
+ {
161
+ "b": "default value",
162
+ "c": "prefault value",
163
+ "d": "catch value",
164
+ "e": "default value",
165
+ "f": "prefault value",
166
+ }
167
+ `);
168
+
169
+ expect(mySchema.parse({}, { jitless: true })).toMatchInlineSnapshot(`
170
+ {
171
+ "b": "default value",
172
+ "c": "prefault value",
173
+ "d": "catch value",
174
+ "e": "default value",
175
+ "f": "prefault value",
176
+ }
177
+ `);
178
+ });
179
+
180
+ test("handleOptionalObjectResult branches", () => {
181
+ const mySchema = z.object({
182
+ // Branch: input[key] === undefined, key not in input, caught error
183
+ caughtMissing: z.string().catch("caught").optional(),
184
+ // Branch: input[key] === undefined, key in input, caught error
185
+ caughtUndefined: z.string().catch("caught").optional(),
186
+ // Branch: input[key] === undefined, key not in input, validation issues
187
+ issueMissing: z.string().min(5).optional(),
188
+ // Branch: input[key] === undefined, key in input, validation issues
189
+ issueUndefined: z.string().min(5).optional(),
190
+ // Branch: input[key] === undefined, validation returns undefined
191
+ validUndefined: z.string().optional(),
192
+ // Branch: input[key] === undefined, non-undefined result (default/transform)
193
+ defaultValue: z.string().default("default").optional(),
194
+ // Branch: input[key] defined, caught error
195
+ caughtDefined: z.string().catch("caught").optional(),
196
+ // Branch: input[key] defined, validation issues
197
+ issueDefined: z.string().min(5).optional(),
198
+ // Branch: input[key] defined, validation returns undefined
199
+ validDefinedUndefined: z
200
+ .string()
201
+ .transform(() => undefined)
202
+ .optional(),
203
+ // Branch: input[key] defined, non-undefined value
204
+ validDefined: z.string().optional(),
205
+ });
206
+
207
+ // Test input[key] === undefined cases
208
+ const result1 = mySchema.parse(
209
+ {
210
+ // caughtMissing: not present (key not in input)
211
+ caughtUndefined: undefined, // key in input
212
+ // issueMissing: not present (key not in input)
213
+ issueUndefined: undefined, // key in input
214
+ validUndefined: undefined,
215
+ // defaultValue: not present, will get default
216
+ },
217
+ { jitless: true }
218
+ );
219
+
220
+ expect(result1).toEqual({
221
+ caughtUndefined: undefined,
222
+ issueUndefined: undefined,
223
+ validUndefined: undefined,
224
+ defaultValue: "default",
225
+ });
226
+
227
+ // Test input[key] defined cases (successful)
228
+ const result2 = mySchema.parse(
229
+ {
230
+ caughtDefined: 123, // invalid type, should catch
231
+ validDefinedUndefined: "test", // transforms to undefined
232
+ validDefined: "valid", // valid value
233
+ },
234
+ { jitless: true }
235
+ );
236
+
237
+ expect(result2).toEqual({
238
+ caughtDefined: "caught",
239
+ validDefinedUndefined: undefined,
240
+ validDefined: "valid",
241
+ defaultValue: "default",
242
+ });
243
+
244
+ // Test validation issues are properly reported (input[key] defined, validation fails)
245
+ expect(() =>
246
+ mySchema.parse(
247
+ {
248
+ issueDefined: "abc", // too short
249
+ },
250
+ { jitless: true }
251
+ )
252
+ ).toThrow();
253
+ });
254
+
255
+ test("fastpass vs non-fastpass consistency", () => {
256
+ const mySchema = z.object({
257
+ caughtMissing: z.string().catch("caught").optional(),
258
+ caughtUndefined: z.string().catch("caught").optional(),
259
+ issueMissing: z.string().min(5).optional(),
260
+ issueUndefined: z.string().min(5).optional(),
261
+ validUndefined: z.string().optional(),
262
+ defaultValue: z.string().default("default").optional(),
263
+ caughtDefined: z.string().catch("caught").optional(),
264
+ validDefinedUndefined: z
265
+ .string()
266
+ .transform(() => undefined)
267
+ .optional(),
268
+ validDefined: z.string().optional(),
269
+ });
270
+
271
+ const input = {
272
+ caughtUndefined: undefined,
273
+ issueUndefined: undefined,
274
+ validUndefined: undefined,
275
+ caughtDefined: 123,
276
+ validDefinedUndefined: "test",
277
+ validDefined: "valid",
278
+ };
279
+
280
+ // Test both paths produce identical results
281
+ const jitlessResult = mySchema.parse(input, { jitless: true });
282
+ const fastpassResult = mySchema.parse(input);
283
+
284
+ expect(jitlessResult).toEqual(fastpassResult);
285
+ expect(jitlessResult).toEqual({
286
+ caughtUndefined: undefined,
287
+ issueUndefined: undefined,
288
+ validUndefined: undefined,
289
+ defaultValue: "default",
290
+ caughtDefined: "caught",
291
+ validDefinedUndefined: undefined,
292
+ validDefined: "valid",
293
+ });
294
+ });
295
+
296
+ test("optional with check", () => {
297
+ const baseSchema = z
298
+ .string()
299
+ .optional()
300
+ .check(({ value, ...ctx }) => {
301
+ ctx.issues.push({
302
+ code: "custom",
303
+ input: value,
304
+ message: "message",
305
+ });
306
+ });
307
+
308
+ // this correctly fails
309
+ expect(baseSchema.safeParse(undefined)).toMatchInlineSnapshot(`
310
+ {
311
+ "error": [ZodError: [
312
+ {
313
+ "code": "custom",
314
+ "message": "message",
315
+ "path": []
316
+ }
317
+ ]],
318
+ "success": false,
319
+ }
320
+ `);
321
+
322
+ const schemaObject = z.object({
323
+ date: baseSchema,
324
+ });
325
+
326
+ expect(schemaObject.safeParse({ date: undefined })).toMatchInlineSnapshot(`
327
+ {
328
+ "error": [ZodError: [
329
+ {
330
+ "code": "custom",
331
+ "message": "message",
332
+ "path": [
333
+ "date"
334
+ ]
335
+ }
336
+ ]],
337
+ "success": false,
338
+ }
339
+ `);
340
+ });
@@ -114,14 +114,14 @@ test("pick/omit/required/partial - do not allow unknown keys", () => {
114
114
  age: z.number(),
115
115
  });
116
116
 
117
- expect(() => schema.pick({ name: true, asdf: true })).toThrow();
117
+ expect(() => schema.pick({ name: true, asdf: true }).safeParse({})).toThrow();
118
118
 
119
119
  // @ts-expect-error
120
- expect(() => schema.pick({ $unknown: true })).toThrow();
120
+ expect(() => schema.pick({ $unknown: true }).safeParse({})).toThrow();
121
121
  // @ts-expect-error
122
- expect(() => schema.omit({ $unknown: true })).toThrow();
122
+ expect(() => schema.omit({ $unknown: true }).safeParse({})).toThrow();
123
123
  // @ts-expect-error
124
- expect(() => schema.required({ $unknown: true })).toThrow();
124
+ expect(() => schema.required({ $unknown: true }).safeParse({})).toThrow();
125
125
  // @ts-expect-error
126
- expect(() => schema.partial({ $unknown: true })).toThrow();
126
+ expect(() => schema.partial({ $unknown: true }).safeParse({})).toThrow();
127
127
  });
@@ -73,17 +73,18 @@ test("preprocess ctx.addIssue with parse", () => {
73
73
  `);
74
74
  });
75
75
 
76
- test("preprocess ctx.addIssue non-fatal by default", () => {
76
+ test("preprocess ctx.addIssue fatal by default", () => {
77
77
  const schema = z.preprocess((data, ctx) => {
78
78
  ctx.addIssue({
79
79
  code: "custom",
80
80
  message: `custom error`,
81
81
  });
82
+
82
83
  return data;
83
84
  }, z.string());
84
85
  const result = schema.safeParse(1234);
85
86
 
86
- expect(result.error!.issues).toHaveLength(2);
87
+ expect(result.error!.issues).toHaveLength(1);
87
88
  expect(result).toMatchInlineSnapshot(`
88
89
  {
89
90
  "error": [ZodError: [
@@ -91,12 +92,6 @@ test("preprocess ctx.addIssue non-fatal by default", () => {
91
92
  "code": "custom",
92
93
  "message": "custom error",
93
94
  "path": []
94
- },
95
- {
96
- "expected": "string",
97
- "code": "invalid_type",
98
- "path": [],
99
- "message": "Invalid input: expected string, received number"
100
95
  }
101
96
  ]],
102
97
  "success": false,
@@ -175,7 +170,7 @@ test("z.NEVER in preprocess", () => {
175
170
  expectTypeOf<foo>().toEqualTypeOf<number>();
176
171
  const result = foo.safeParse(undefined);
177
172
 
178
- expect(result.error!.issues).toHaveLength(2);
173
+ expect(result.error!.issues).toHaveLength(1);
179
174
  expect(result).toMatchInlineSnapshot(`
180
175
  {
181
176
  "error": [ZodError: [
@@ -183,12 +178,6 @@ test("z.NEVER in preprocess", () => {
183
178
  "code": "custom",
184
179
  "message": "bad",
185
180
  "path": []
186
- },
187
- {
188
- "expected": "number",
189
- "code": "invalid_type",
190
- "path": [],
191
- "message": "Invalid input: expected number, received object"
192
181
  }
193
182
  ]],
194
183
  "success": false,
@@ -336,7 +336,21 @@ test("partial record", () => {
336
336
  type schema = z.infer<typeof schema>;
337
337
  expectTypeOf<schema>().toEqualTypeOf<Partial<Record<string, string>>>();
338
338
 
339
- const Keys = z.enum(["id", "name", "email"]).or(z.never());
339
+ const Keys = z.enum(["id", "name", "email"]); //.or(z.never());
340
340
  const Person = z.partialRecord(Keys, z.string());
341
341
  expectTypeOf<z.infer<typeof Person>>().toEqualTypeOf<Partial<Record<"id" | "name" | "email", string>>>();
342
+
343
+ Person.parse({
344
+ id: "123",
345
+ // name: "John",
346
+ // email: "john@example.com",
347
+ });
348
+
349
+ Person.parse({
350
+ // id: "123",
351
+ // name: "John",
352
+ email: "john@example.com",
353
+ });
354
+
355
+ expect(Person.def.keyType._zod.def.type).toEqual("enum");
342
356
  });
@@ -260,6 +260,73 @@ test("mutual recursion with meta", () => {
260
260
  expectTypeOf<B>().toEqualTypeOf<_B>();
261
261
  });
262
262
 
263
+ test("object utilities with recursive types", () => {
264
+ const NodeBase = z.object({
265
+ id: z.string(),
266
+ name: z.string(),
267
+ get children() {
268
+ return z.array(Node).optional();
269
+ },
270
+ });
271
+
272
+ // Test extend
273
+ const NodeOne = NodeBase.extend({
274
+ name: z.literal("nodeOne"),
275
+ get children() {
276
+ return z.array(Node);
277
+ },
278
+ });
279
+
280
+ const NodeTwo = NodeBase.extend({
281
+ name: z.literal("nodeTwo"),
282
+ get children() {
283
+ return z.array(Node);
284
+ },
285
+ });
286
+
287
+ // Test pick
288
+ const PickedNode = NodeBase.pick({ id: true, name: true });
289
+
290
+ // Test omit
291
+ const OmittedNode = NodeBase.omit({ children: true });
292
+
293
+ // Test merge
294
+ const ExtraProps = {
295
+ metadata: z.string(),
296
+ get parent() {
297
+ return Node.optional();
298
+ },
299
+ };
300
+ const MergedNode = NodeBase.extend(ExtraProps);
301
+
302
+ // Test partial
303
+ const PartialNode = NodeBase.partial();
304
+ const PartialMaskedNode = NodeBase.partial({ name: true });
305
+
306
+ // Test required (assuming NodeBase has optional fields)
307
+ const OptionalNodeBase = z.object({
308
+ id: z.string().optional(),
309
+ name: z.string().optional(),
310
+ get children() {
311
+ return z.array(Node).optional();
312
+ },
313
+ });
314
+ const RequiredNode = OptionalNodeBase.required();
315
+ const RequiredMaskedNode = OptionalNodeBase.required({ id: true });
316
+
317
+ const Node = z.union([
318
+ NodeOne,
319
+ NodeTwo,
320
+ PickedNode,
321
+ OmittedNode,
322
+ MergedNode,
323
+ PartialNode,
324
+ PartialMaskedNode,
325
+ RequiredNode,
326
+ RequiredMaskedNode,
327
+ ]);
328
+ });
329
+
263
330
  test("recursion compatibility", () => {
264
331
  // array
265
332
  const A = z.object({
@@ -318,6 +318,83 @@ test("url validations", () => {
318
318
  expect(() => url.parse("https://")).toThrow();
319
319
  });
320
320
 
321
+ test("url preserves original input", () => {
322
+ const url = z.string().url();
323
+
324
+ // Test the specific case from the user report
325
+ const input = "https://example.com?key=NUXOmHqWNVTapJkJJHw8BfD155AuqhH_qju_5fNmQ4ZHV7u8";
326
+ const output = url.parse(input);
327
+ expect(output).toBe(input); // Should preserve the original input exactly
328
+
329
+ // Test other cases where URL constructor would normalize
330
+ expect(url.parse("https://example.com?foo=bar")).toBe("https://example.com?foo=bar");
331
+ expect(url.parse("http://example.com?test=123")).toBe("http://example.com?test=123");
332
+ expect(url.parse("https://sub.example.com?param=value&other=data")).toBe(
333
+ "https://sub.example.com?param=value&other=data"
334
+ );
335
+
336
+ // Test cases with trailing slashes are preserved
337
+ expect(url.parse("https://example.com/")).toBe("https://example.com/");
338
+ expect(url.parse("https://example.com/path/")).toBe("https://example.com/path/");
339
+
340
+ // Test cases with paths and query parameters
341
+ expect(url.parse("https://example.com/path?query=param")).toBe("https://example.com/path?query=param");
342
+ });
343
+
344
+ test("url trims whitespace", () => {
345
+ const url = z.string().url();
346
+
347
+ // Test trimming whitespace from URLs
348
+ expect(url.parse(" https://example.com ")).toBe("https://example.com");
349
+ expect(url.parse(" https://example.com/path?query=param ")).toBe("https://example.com/path?query=param");
350
+ expect(url.parse("\t\nhttps://example.com\t\n")).toBe("https://example.com");
351
+ expect(url.parse(" https://example.com?key=value ")).toBe("https://example.com?key=value");
352
+
353
+ // Test that URLs without extra whitespace are unchanged
354
+ expect(url.parse("https://example.com")).toBe("https://example.com");
355
+ expect(url.parse("https://example.com/path")).toBe("https://example.com/path");
356
+ });
357
+
358
+ test("url normalize flag", () => {
359
+ const normalizeUrl = z.url({ normalize: true });
360
+ const preserveUrl = z.url(); // normalize: false/undefined by default
361
+
362
+ // Test that normalize flag causes URL normalization
363
+ expect(normalizeUrl.parse("https://example.com?key=value")).toBe("https://example.com/?key=value");
364
+ expect(normalizeUrl.parse("http://example.com?test=123")).toBe("http://example.com/?test=123");
365
+
366
+ // Test with already normalized URLs
367
+ expect(normalizeUrl.parse("https://example.com/")).toBe("https://example.com/");
368
+ expect(normalizeUrl.parse("https://example.com/path?query=param")).toBe("https://example.com/path?query=param");
369
+
370
+ // Test complex URLs with normalization
371
+ expect(normalizeUrl.parse("https://example.com/../?key=value")).toBe("https://example.com/?key=value");
372
+ expect(normalizeUrl.parse("https://example.com/./path?key=value")).toBe("https://example.com/path?key=value");
373
+
374
+ // Compare with non-normalize behavior
375
+ expect(preserveUrl.parse("https://example.com?key=value")).toBe("https://example.com?key=value");
376
+ expect(preserveUrl.parse("http://example.com?test=123")).toBe("http://example.com?test=123");
377
+
378
+ // Test trimming with normalize
379
+ expect(normalizeUrl.parse(" https://example.com?key=value ")).toBe("https://example.com/?key=value");
380
+ expect(preserveUrl.parse(" https://example.com?key=value ")).toBe("https://example.com?key=value");
381
+ });
382
+
383
+ test("url normalize with hostname and protocol constraints", () => {
384
+ const constrainedNormalizeUrl = z.url({
385
+ normalize: true,
386
+ protocol: /^https$/,
387
+ hostname: /^example\.com$/,
388
+ });
389
+
390
+ // Test that normalization works with constraints
391
+ expect(constrainedNormalizeUrl.parse("https://example.com?key=value")).toBe("https://example.com/?key=value");
392
+
393
+ // Test that constraints are still enforced
394
+ expect(() => constrainedNormalizeUrl.parse("http://example.com?key=value")).toThrow();
395
+ expect(() => constrainedNormalizeUrl.parse("https://other.com?key=value")).toThrow();
396
+ });
397
+
321
398
  test("httpurl", () => {
322
399
  const httpUrl = z.url({
323
400
  protocol: /^https?$/,
@@ -1952,6 +1952,7 @@ test("input type", () => {
1952
1952
  "required": [
1953
1953
  "a",
1954
1954
  "d",
1955
+ "f",
1955
1956
  "g",
1956
1957
  ],
1957
1958
  "type": "object",