zod 4.6.2 → 4.6.3

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 (66) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/src/v4/classic/checks.ts +1 -0
  4. package/src/v4/classic/schemas.ts +3 -25
  5. package/src/v4/classic/tests/cyclic-data.test.ts +2 -11
  6. package/src/v4/classic/tests/firstparty.test.ts +0 -4
  7. package/src/v4/classic/tests/instanceof.test.ts +1 -1
  8. package/src/v4/classic/tests/properties.test.ts +34 -137
  9. package/src/v4/core/api.ts +4 -7
  10. package/src/v4/core/checks.ts +61 -1
  11. package/src/v4/core/compile.ts +8 -18
  12. package/src/v4/core/json-schema-processors.ts +0 -50
  13. package/src/v4/core/memoizer.ts +2 -5
  14. package/src/v4/core/schemas.ts +1 -98
  15. package/src/v4/core/to-json-schema.ts +1 -1
  16. package/src/v4/core/util.ts +0 -4
  17. package/src/v4/core/versions.ts +1 -1
  18. package/src/v4/core/visit.ts +0 -11
  19. package/src/v4/mini/checks.ts +1 -0
  20. package/src/v4/mini/schemas.ts +0 -19
  21. package/v4/classic/checks.cjs +2 -1
  22. package/v4/classic/checks.d.cts +1 -1
  23. package/v4/classic/checks.d.ts +1 -1
  24. package/v4/classic/checks.js +1 -1
  25. package/v4/classic/schemas.cjs +2 -11
  26. package/v4/classic/schemas.d.cts +1 -6
  27. package/v4/classic/schemas.d.ts +1 -6
  28. package/v4/classic/schemas.js +1 -9
  29. package/v4/core/api.cjs +2 -3
  30. package/v4/core/api.d.cts +2 -2
  31. package/v4/core/api.d.ts +2 -2
  32. package/v4/core/api.js +2 -3
  33. package/v4/core/checks.cjs +32 -1
  34. package/v4/core/checks.d.cts +16 -0
  35. package/v4/core/checks.d.ts +16 -0
  36. package/v4/core/checks.js +31 -0
  37. package/v4/core/compile.cjs +5 -11
  38. package/v4/core/compile.js +5 -11
  39. package/v4/core/json-schema-processors.cjs +1 -34
  40. package/v4/core/json-schema-processors.d.cts +0 -1
  41. package/v4/core/json-schema-processors.d.ts +0 -1
  42. package/v4/core/json-schema-processors.js +1 -33
  43. package/v4/core/memoizer.cjs +2 -5
  44. package/v4/core/memoizer.js +2 -5
  45. package/v4/core/schemas.cjs +1 -62
  46. package/v4/core/schemas.d.cts +2 -19
  47. package/v4/core/schemas.d.ts +2 -19
  48. package/v4/core/schemas.js +0 -61
  49. package/v4/core/to-json-schema.cjs +0 -1
  50. package/v4/core/to-json-schema.d.cts +0 -3
  51. package/v4/core/to-json-schema.d.ts +0 -3
  52. package/v4/core/to-json-schema.js +1 -1
  53. package/v4/core/util.cjs +0 -4
  54. package/v4/core/util.js +0 -4
  55. package/v4/core/versions.cjs +1 -1
  56. package/v4/core/versions.js +1 -1
  57. package/v4/core/visit.cjs +0 -12
  58. package/v4/core/visit.js +0 -12
  59. package/v4/mini/checks.cjs +2 -1
  60. package/v4/mini/checks.d.cts +1 -1
  61. package/v4/mini/checks.d.ts +1 -1
  62. package/v4/mini/checks.js +1 -1
  63. package/v4/mini/schemas.cjs +1 -9
  64. package/v4/mini/schemas.d.cts +0 -4
  65. package/v4/mini/schemas.d.ts +0 -4
  66. package/v4/mini/schemas.js +0 -7
package/README.md CHANGED
@@ -155,13 +155,13 @@ try {
155
155
  expected: 'string',
156
156
  code: 'invalid_type',
157
157
  path: [ 'username' ],
158
- message: 'Invalid input: expected string'
158
+ message: 'Invalid input: expected string, received number'
159
159
  },
160
160
  {
161
161
  expected: 'number',
162
162
  code: 'invalid_type',
163
163
  path: [ 'xp' ],
164
- message: 'Invalid input: expected number'
164
+ message: 'Invalid input: expected number, received string'
165
165
  }
166
166
  ] */
167
167
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zod",
3
- "version": "4.6.2",
3
+ "version": "4.6.3",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "author": "Colin McDonnell <zod@colinhacks.com>",
@@ -21,6 +21,7 @@ export {
21
21
  _startsWith as startsWith,
22
22
  _endsWith as endsWith,
23
23
  _property as property,
24
+ _properties as properties,
24
25
  _mime as mime,
25
26
  _overwrite as overwrite,
26
27
  _normalize as normalize,
@@ -2791,28 +2791,6 @@ export const ZodCustom: core.$constructor<ZodCustom> = /*@__PURE__*/ core.$const
2791
2791
  inst._zod.processJSONSchema = (ctx, json, params) => processors.customProcessor(inst, ctx, json, params);
2792
2792
  });
2793
2793
 
2794
- // ZodProperties
2795
- export interface ZodProperties<Shape extends core.$ZodShape = core.$ZodShape>
2796
- extends _ZodType<core.$ZodPropertiesInternals<Shape>>,
2797
- core.$ZodProperties<Shape> {
2798
- "~standard": ZodStandardSchemaWithJSON<this>;
2799
- }
2800
- export const ZodProperties: core.$constructor<ZodProperties> = /*@__PURE__*/ core.$constructor(
2801
- "ZodProperties",
2802
- (inst, def) => {
2803
- _ensureDefaultMemoizer();
2804
- core.$ZodProperties.init(inst, def);
2805
- ZodType.init(inst, def);
2806
- }
2807
- );
2808
-
2809
- export function properties<Shape extends core.$ZodShape>(
2810
- shape: Shape,
2811
- params?: string | core.$ZodPropertiesParams
2812
- ): ZodProperties<Shape> {
2813
- return core._properties(ZodProperties, shape, params) as any;
2814
- }
2815
-
2816
2794
  // custom checks
2817
2795
  export function check<O = unknown>(fn: core.CheckFn<O>): core.$ZodCheck<O> {
2818
2796
  const ch = new core.$ZodCheck({
@@ -2860,7 +2838,7 @@ type ZodInstanceOfParams = core.Params<
2860
2838
  export interface ZodInstanceOf<T = unknown> extends ZodCustom<T, T> {
2861
2839
  properties<Shape extends core.$ZodShape>(
2862
2840
  shape: Shape,
2863
- params?: string | core.$ZodPropertiesParams
2841
+ params?: string | core.$ZodCheckPropertiesParams
2864
2842
  ): ZodInstanceOf<T & core.$InferObjectInput<Shape, {}>>;
2865
2843
  }
2866
2844
  export const ZodInstanceOf: core.$constructor<ZodInstanceOf> = /*@__PURE__*/ core.$constructor(
@@ -2869,9 +2847,9 @@ export const ZodInstanceOf: core.$constructor<ZodInstanceOf> = /*@__PURE__*/ cor
2869
2847
  ZodCustom.init(inst, def);
2870
2848
  },
2871
2849
  {
2872
- properties(shape: core.$ZodShape, params?: string | core.$ZodPropertiesParams) {
2850
+ properties(shape: core.$ZodShape, params?: string | core.$ZodCheckPropertiesParams) {
2873
2851
  // asserts in place, so the narrowed output type is truthful without a wrapper
2874
- return this.check(properties(shape, params)) as any;
2852
+ return this.check(core._properties(shape, params)) as any;
2875
2853
  },
2876
2854
  }
2877
2855
  );
@@ -924,17 +924,8 @@ test("a deferred edge stops being assumed a cycle once the graph resolves", () =
924
924
  expect(z.core.isRecursiveSchema(Cyclic)).toBe(true);
925
925
  });
926
926
 
927
- // `z.object` resolves its shape by spread, so a non-enumerable key is no part of it; `z.properties` reads every own key, so one there is still parsed and can still close a cycle
928
- test("a non-enumerable key counts for z.properties and not for z.object", () => {
929
- const props = (enumerable: boolean) => {
930
- const shape: Record<string, any> = { id: z.number() };
931
- const P = z.properties(shape);
932
- Object.defineProperty(shape, "next", { value: z.lazy(() => z.optional(P)), enumerable, configurable: true });
933
- return P;
934
- };
935
- expect(z.core.isRecursiveSchema(props(true) as any)).toBe(true);
936
- expect(z.core.isRecursiveSchema(props(false) as any)).toBe(true);
937
-
927
+ // `z.object` resolves its shape by spread, so a non-enumerable key is no part of it
928
+ test("a non-enumerable key does not count for z.object", () => {
938
929
  const object = (enumerable: boolean) => {
939
930
  const shape: Record<string, any> = { id: z.number() };
940
931
  const O: any = z.object(shape);
@@ -80,8 +80,6 @@ test("first party switch", () => {
80
80
  break;
81
81
  case "file":
82
82
  break;
83
- case "properties":
84
- break;
85
83
  case "lazy":
86
84
  break;
87
85
  case "function":
@@ -172,8 +170,6 @@ test("$ZodSchemaTypes", () => {
172
170
  break;
173
171
  case "lazy":
174
172
  break;
175
- case "properties":
176
- break;
177
173
  case "function":
178
174
  break;
179
175
 
@@ -90,7 +90,7 @@ test("z.properties", () => {
90
90
  expect(obj.safeParse(input).error!.issues.map((i) => [i.code, i.path])).toEqual([["invalid_type", []]]);
91
91
  }
92
92
 
93
- // Known looseness versus the longhand, pinned so it stays deliberate: the schema-as-check is typed over the whole shape, and `$ZodCheckInternals.check()` is a method, so TypeScript compares it bivariantly and accepts a check type that is a subtype of the target. Naming a key the target lacks therefore compiles here and fails at parse time, where the equivalent chain of `z.property()` calls rejects it outright.
93
+ // Known looseness versus the longhand, pinned so it stays deliberate: the check is typed over the whole shape, and `$ZodCheckInternals.check()` is a method, so TypeScript compares it bivariantly and accepts a check type that is a subtype of the target. Naming a key the target lacks therefore compiles here and fails at parse time, where the equivalent chain of `z.property()` calls rejects it outright.
94
94
  z.object({ a: z.string() }).check(...z.properties({ a: z.literal("x"), b: z.literal("y") }));
95
95
  expect(
96
96
  z
@@ -1,25 +1,15 @@
1
1
  import { expect, expectTypeOf, test } from "vitest";
2
2
  import * as z from "zod/v4";
3
3
 
4
- test("z.properties is a schema", () => {
5
- const p = z.properties({ a: z.literal("x"), b: z.literal("y") });
4
+ test("z.properties is a check", () => {
5
+ const s = z.object({ a: z.string(), b: z.string() }).check(z.properties({ a: z.literal("x"), b: z.literal("y") }));
6
6
 
7
- expectTypeOf<z.infer<typeof p>>().toEqualTypeOf<{ a: "x"; b: "y" }>();
7
+ // asserts in place: extra keys pass, and every failing key reports
8
+ expect(s.safeParse({ a: "x", b: "y", extra: 1 }).success).toBe(true);
9
+ expect(s.safeParse({ a: "!", b: "!" }).error!.issues.map((i) => i.path)).toEqual([["a"], ["b"]]);
8
10
 
9
- // asserts in place: the parse returns the input identity, extra keys pass
10
- const ok = { a: "x", b: "y", extra: 1 };
11
- expect(p.parse(ok)).toBe(ok);
12
-
13
- // every failing key reports
14
- expect(p.safeParse({ a: "!", b: "!" }).error!.issues.map((i) => i.path)).toEqual([["a"], ["b"]]);
15
-
16
- // as a schema it is the type gate and infers an object shape, so a primitive is a type error
17
- for (const input of [null, undefined, 5, "abc"]) {
18
- expect(p.safeParse(input).error!.issues.map((i) => [i.code, i.path])).toEqual([["invalid_type", []]]);
19
- }
20
-
21
- // as a check the base already typed the value, so the properties are asserted on whatever it produced — a string's length, as z.property() does
22
- const long = z.string().check(...z.properties({ length: z.number().min(3) }));
11
+ // the base already typed the value, so the properties are asserted on whatever it produced — a string's length, as z.property() does
12
+ const long = z.string().check(z.properties({ length: z.number().min(3) }));
23
13
  expect(long.parse("abc")).toBe("abc");
24
14
  expect(long.safeParse("ab").error!.issues.map((i) => i.path)).toEqual([["length"]]);
25
15
  expect(
@@ -29,29 +19,19 @@ test("z.properties is a schema", () => {
29
19
  .error!.issues.map((i) => i.path)
30
20
  ).toEqual([["length"]]);
31
21
 
32
- // an optional key may be absent
33
- const opt = z.properties({ a: z.string().optional() });
34
- expect(opt.safeParse({}).success).toBe(true);
35
- expectTypeOf<z.infer<typeof opt>>().toEqualTypeOf<{ a?: string | undefined }>();
22
+ // not a schema
23
+ // @ts-expect-error a check has no parse of its own
24
+ z.properties({ a: z.string() }).parse;
36
25
  });
37
26
 
38
27
  test("z.properties discards transformed output", () => {
39
28
  // asserts and never writes back, exactly as z.property() behaves; a nested object schema rebuilds its output even without transforms, so identity cannot distinguish the two
40
- const p = z.properties({ a: z.string().transform((s) => s.toUpperCase()) });
29
+ const s = z.any().check(z.properties({ a: z.string().transform((s) => s.toUpperCase()) }));
41
30
  const input = { a: "hi" };
42
- expect(p.parse(input)).toBe(input);
31
+ expect(s.parse(input)).toBe(input);
43
32
  expect(input.a).toBe("hi");
44
33
  });
45
34
 
46
- test("z.properties narrows through intersection", () => {
47
- const httpsUrl = z.instanceof(URL).and(z.properties({ protocol: z.literal("https:") }));
48
- expectTypeOf<z.infer<typeof httpsUrl>>().toEqualTypeOf<URL & { protocol: "https:" }>();
49
-
50
- const u = new URL("https://example.com");
51
- expect(httpsUrl.parse(u)).toBe(u);
52
- expect(httpsUrl.safeParse(new URL("http://example.com")).success).toBe(false);
53
- });
54
-
55
35
  test("z.instanceof().properties()", () => {
56
36
  const httpsUrl = z.instanceof(URL).properties({ protocol: z.literal("https:") });
57
37
  expectTypeOf<z.infer<typeof httpsUrl>>().toEqualTypeOf<URL & { protocol: "https:" }>();
@@ -65,38 +45,23 @@ test("z.instanceof().properties()", () => {
65
45
  expectTypeOf<z.infer<typeof chained>>().toEqualTypeOf<URL & { protocol: "https:" } & { port: "" }>();
66
46
  expect(chained.safeParse(u).success).toBe(true);
67
47
  expect(chained.safeParse(new URL("https://example.com:8443")).error!.issues.map((i) => i.path)).toEqual([["port"]]);
48
+
49
+ // nothing is written back, so the narrowing is over the shape's input side
50
+ const W = z.instanceof(URL).properties({ search: z.string().transform((s) => s.length) });
51
+ expectTypeOf<z.infer<typeof W>>().toEqualTypeOf<URL & { search: string }>();
68
52
  });
69
53
 
70
54
  test("z.properties spreads into .check()", () => {
71
- // the pre-4.6 array call sites: the schema yields itself from Symbol.iterator
55
+ // the 4.5 array call sites: the check yields itself from Symbol.iterator
72
56
  const p = z.properties({ a: z.literal("x") });
73
- const spread = [...p];
74
- expect(spread).toEqual([p]);
57
+ expect([...p]).toEqual([p]);
75
58
 
76
59
  const s = z.object({ a: z.string() }).check(...z.properties({ a: z.literal("x") }));
77
60
  expect(s.safeParse({ a: "x" }).success).toBe(true);
78
61
  expect(s.safeParse({ a: "!" }).error!.issues.map((i) => i.path)).toEqual([["a"]]);
79
62
  });
80
63
 
81
- test("z.properties JSON Schema", () => {
82
- const p = z.properties({ a: z.string(), b: z.number().optional() });
83
- expect(z.toJSONSchema(p)).toEqual({
84
- $schema: "https://json-schema.org/draft/2020-12/schema",
85
- type: "object",
86
- properties: { a: { type: "string" }, b: { type: "number" } },
87
- required: ["a"],
88
- });
89
- });
90
-
91
- test("z.properties compiles", () => {
92
- const p = z.properties({ a: z.literal("x"), b: z.literal("y") });
93
- const compiled = z.compile(p);
94
- const ok = { a: "x", b: "y", extra: 1 };
95
- expect(compiled.parse(ok)).toBe(ok);
96
- expect(compiled.safeParse({ a: "!", b: "!" }).error!.issues.map((i) => i.path)).toEqual([["a"], ["b"]]);
97
- expect(compiled.safeParse(null).error!.issues.map((i) => [i.code, i.path])).toEqual([["invalid_type", []]]);
98
-
99
- // nested in a container
64
+ test("z.properties compiles inside a container", () => {
100
65
  const outer = z.compile(z.object({ url: z.instanceof(URL).properties({ protocol: z.literal("https:") }) }));
101
66
  expect(outer.safeParse({ url: new URL("http://example.com") }).error!.issues.map((i) => i.path)).toEqual([
102
67
  ["url", "protocol"],
@@ -104,43 +69,28 @@ test("z.properties compiles", () => {
104
69
  });
105
70
 
106
71
  test("z.properties async", async () => {
107
- const p = z.properties({ a: z.string().refine(async (s) => s.length > 1) });
72
+ const s = z.any().check(z.properties({ a: z.string().refine(async (s) => s.length > 1) }));
108
73
  const ok = { a: "hi" };
109
- await expect(p.parseAsync(ok)).resolves.toBe(ok);
110
- const bad = await p.safeParseAsync({ a: "!" });
74
+ await expect(s.parseAsync(ok)).resolves.toBe(ok);
75
+ const bad = await s.safeParseAsync({ a: "!" });
111
76
  expect(bad.error!.issues.map((i) => i.path)).toEqual([["a"]]);
112
77
  });
113
78
 
114
- test("z.properties infers the input side", () => {
115
- // nothing is written back, so an output-typed inference would lie for every shape entry whose output differs from its input
116
- const withDefault = z.properties({ a: z.string().default("x") });
117
- expectTypeOf<z.infer<typeof withDefault>>().toEqualTypeOf<{ a?: string | undefined }>();
118
- expect(withDefault.parse({})).toEqual({});
119
-
120
- const withTransform = z.properties({ a: z.string().transform((s) => s.length) });
121
- expectTypeOf<z.infer<typeof withTransform>>().toEqualTypeOf<{ a: string }>();
122
- expect(withTransform.parse({ a: "hi" })).toEqual({ a: "hi" });
123
-
124
- // a plain schema has the same input and output, so the documented narrowing is unchanged
125
- const W = z.instanceof(URL).properties({ protocol: z.literal("https:") });
126
- expectTypeOf<z.infer<typeof W>>().toEqualTypeOf<URL & { protocol: "https:" }>();
127
- });
128
-
129
79
  test("z.properties validates symbol keys", () => {
130
80
  const sym = Symbol("tag");
131
- const p = z.properties({ [sym]: z.string() });
132
- expect(p.safeParse({ [sym]: "ok" }).success).toBe(true);
133
- expect(p.safeParse({ [sym]: 123 }).error!.issues.map((i) => i.path)).toEqual([[sym]]);
81
+ const s = z.any().check(z.properties({ [sym]: z.string() }));
82
+ expect(s.safeParse({ [sym]: "ok" }).success).toBe(true);
83
+ expect(s.safeParse({ [sym]: 123 }).error!.issues.map((i) => i.path)).toEqual([[sym]]);
134
84
  expect(
135
85
  z
136
- .compile(p)
86
+ .compile(s)
137
87
  .safeParse({ [sym]: 123 })
138
88
  .error!.issues.map((i) => i.path)
139
89
  ).toEqual([[sym]]);
140
90
  });
141
91
 
142
92
  test("z.properties compiled and interpreted agree on a nullish value", () => {
143
- // a base that permits null reaches the check role with one; the compiled property read must not throw where the runtime reports an issue
93
+ // a base that permits null reaches the check with one; the compiled property read must not throw where the runtime reports an issue
144
94
  const s = z.any().check(...z.properties({ a: z.string() }));
145
95
  const expected = [["invalid_type", []]];
146
96
  expect(s.safeParse(null).error!.issues.map((i) => [i.code, i.path])).toEqual(expected);
@@ -153,73 +103,20 @@ test("z.properties compiled and interpreted agree on a nullish value", () => {
153
103
  });
154
104
 
155
105
  test("z.properties with a custom when refuses to compile", () => {
156
- // `when` is not publicly settable, since it gates a check inside the run loop and means nothing to a parsed schema. A hand-built def can still carry one, and compiling it must refuse rather than run the shape unconditionally: a union branch compiles to its own IIFE, so a wrong rejection is absorbed as a branch failure with no fallback.
157
- const p = new z.core.$ZodProperties({
158
- type: "properties",
159
- check: "properties",
160
- shape: { a: z.literal("x") },
161
- when: () => false,
162
- });
163
- const s = z.union([p as unknown as z.ZodType, z.object({ b: z.string() })]);
106
+ // `when` is not publicly settable, since it gates the assertion inside the run loop. A hand-built def can still carry one, and compiling it must refuse rather than run the shape unconditionally: a union branch compiles to its own IIFE, so a wrong rejection is absorbed as a branch failure with no fallback.
107
+ const p = new z.core.$ZodCheckProperties({ check: "properties", shape: { a: z.literal("x") }, when: () => false });
108
+ const s = z.union([z.any().check(p), z.object({ b: z.string() })]);
164
109
  const input = { a: "wrong", b: "hello" };
165
110
  expect(z.compile(s).safeParse(input)).toEqual(s.safeParse(input));
166
111
  });
167
112
 
168
- test("z.properties parses cyclic input", () => {
169
- // the input is its own output, so it registers as its own memo entry; without that a cycle re-enters forever
170
- const Node: z.ZodType<{ id: string; next?: unknown }> = z.lazy(() =>
171
- z.properties({ id: z.string(), next: Node.optional() })
172
- );
173
- const cyclic: Record<string, unknown> = { id: "a" };
174
- cyclic.next = cyclic;
175
- expect(Node.parse(cyclic)).toBe(cyclic);
176
- expect(z.compile(Node).parse(cyclic)).toBe(cyclic);
177
-
178
- // the cycle guard must not swallow a real failure further down
179
- expect(Node.safeParse({ id: "a", next: { id: 1 } }).error!.issues.map((i) => i.path)).toEqual([["next", "id"]]);
180
- });
181
-
182
- test("z.properties asserts forward while encoding", () => {
183
- // both sides declare the shape's input type, so encoding must still check the value against it; running the children backward would encode them and reject that type
184
- const codec = z.codec(z.string(), z.number(), { decode: (s) => Number(s), encode: (n) => String(n) });
185
- const p = z.properties({ a: codec, b: z.string().transform((s) => s.length) });
186
- const value = { a: "1", b: "hi" };
187
- expect(z.decode(p, value)).toBe(value);
188
- expect(z.encode(p, value)).toBe(value);
189
- expect(z.safeEncode(p, { a: 1, b: "hi" } as never).error!.issues.map((i) => i.path)).toEqual([["a"]]);
190
- });
191
-
192
- test("z.properties parses a cyclic callable", () => {
193
- // property reads work on a function too, so the cycle guard has to recognize one as a reference
194
- const Fn: z.ZodType<{ self?: unknown }> = z.lazy(() => z.properties({ self: Fn.optional() }));
195
- const fn = (() => {}) as unknown as Record<string, unknown>;
196
- fn.self = fn;
197
- expect(Fn.parse(fn)).toBe(fn);
198
- });
199
-
200
113
  test("z.properties survives shape mutation", () => {
201
114
  // key and schema are snapshotted together; caching one and reading the other live paired a stale key with a missing schema
202
115
  const shape: Record<string, z.ZodType> = { a: z.string() };
203
- const p = z.properties(shape);
204
- expect(p.safeParse({ a: "x" }).success).toBe(true);
116
+ const s = z.any().check(z.properties(shape));
117
+ expect(s.safeParse({ a: "x" }).success).toBe(true);
205
118
  delete shape.a;
206
- expect(p.safeParse({ a: "x" }).success).toBe(true);
119
+ expect(s.safeParse({ a: "x" }).success).toBe(true);
207
120
  shape.b = z.string();
208
- expect(p.safeParse({ a: "x" }).success).toBe(true);
209
- });
210
-
211
- test("z.properties JSON Schema describes the input side", () => {
212
- // the parsed value is the input, so a defaulted key that stays absent must not be required of the output either
213
- const p = z.properties({ a: z.string().default("x") });
214
- expect(p.parse({})).toEqual({});
215
- expect(z.toJSONSchema(p, { io: "output" })).toEqual({
216
- $schema: "https://json-schema.org/draft/2020-12/schema",
217
- type: "object",
218
- properties: { a: { default: "x", type: "string" } },
219
- });
220
-
221
- // an output mode emission for a transforming child would describe a value this schema never returns
222
- const t = z.properties({ a: z.string().transform((s) => s.length) });
223
- expect(() => z.toJSONSchema(t, { io: "output" })).toThrow(/cannot be represented/);
224
- expect(z.toJSONSchema(t, { io: "input" })).toMatchObject({ properties: { a: { type: "string" } }, required: ["a"] });
121
+ expect(s.safeParse({ a: "x" }).success).toBe(true);
225
122
  });
@@ -1144,16 +1144,13 @@ export function _property<K extends string, T extends schemas.$ZodType>(
1144
1144
  });
1145
1145
  }
1146
1146
 
1147
- // `when` is omitted: it gates a check inside the run loop, so it means nothing when this is parsed as a schema, and honoring it in one role only would diverge silently
1148
- export type $ZodPropertiesParams = CheckTypeParams<schemas.$ZodProperties, "shape" | "when">;
1147
+ export type $ZodCheckPropertiesParams = CheckParams<checks.$ZodCheckProperties, "shape" | "when">;
1149
1148
  // @__NO_SIDE_EFFECTS__
1150
1149
  export function _properties<Shape extends schemas.$ZodShape>(
1151
- Class: util.SchemaClass<schemas.$ZodProperties>,
1152
1150
  shape: Shape,
1153
- params?: string | $ZodPropertiesParams
1154
- ): schemas.$ZodProperties<Shape> {
1155
- return new Class({
1156
- type: "properties",
1151
+ params?: string | $ZodCheckPropertiesParams
1152
+ ): checks.$ZodCheckProperties<Shape> {
1153
+ return new checks.$ZodCheckProperties({
1157
1154
  check: "properties",
1158
1155
  shape,
1159
1156
  ...util.normalizeParams(params),
@@ -1000,7 +1000,7 @@ export const $ZodCheckEndsWith: core.$constructor<$ZodCheckEndsWith> = /*@__PURE
1000
1000
  function handleCheckPropertyResult(
1001
1001
  result: schemas.ParsePayload<unknown>,
1002
1002
  payload: schemas.ParsePayload<unknown>,
1003
- property: string
1003
+ property: string | symbol
1004
1004
  ) {
1005
1005
  if (result.issues.length) {
1006
1006
  payload.issues.push(...util.prefixIssues(property, result.issues));
@@ -1045,6 +1045,66 @@ export const $ZodCheckProperty: core.$constructor<$ZodCheckProperty> = /*@__PURE
1045
1045
  }
1046
1046
  );
1047
1047
 
1048
+ /////////////////////////////////////
1049
+ ///// $ZodCheckProperties /////
1050
+ /////////////////////////////////////
1051
+ export interface $ZodCheckPropertiesDef<Shape extends schemas.$ZodShape = schemas.$ZodShape> extends $ZodCheckDef {
1052
+ check: "properties";
1053
+ shape: Shape;
1054
+ }
1055
+
1056
+ // typed over the input side, since each child's result is discarded; a literal widens to its primitive so spreading over a `string` property still type-checks (#6520)
1057
+ export type $ZodCheckPropertiesInput<Shape extends schemas.$ZodShape = schemas.$ZodShape> = {
1058
+ -readonly [k in keyof Shape]: util.Widen<core.input<Shape[k]>>;
1059
+ };
1060
+
1061
+ export interface $ZodCheckPropertiesInternals<Shape extends schemas.$ZodShape = schemas.$ZodShape>
1062
+ extends $ZodCheckInternals<$ZodCheckPropertiesInput<Shape>> {
1063
+ def: $ZodCheckPropertiesDef<Shape>;
1064
+ issc: errors.$ZodIssue;
1065
+ }
1066
+
1067
+ export interface $ZodCheckProperties<Shape extends schemas.$ZodShape = schemas.$ZodShape>
1068
+ extends $ZodCheck<$ZodCheckPropertiesInput<Shape>> {
1069
+ _zod: $ZodCheckPropertiesInternals<Shape>;
1070
+ // yields the check itself, so `.check(...z.properties(shape))` spread call sites keep working
1071
+ [Symbol.iterator](): Iterator<this>;
1072
+ }
1073
+
1074
+ export const $ZodCheckProperties: core.$constructor<$ZodCheckProperties> = /*@__PURE__*/ core.$constructor(
1075
+ "$ZodCheckProperties",
1076
+ (inst, def) => {
1077
+ $ZodCheck.init(inst, def);
1078
+ util.hide(inst, Symbol.iterator, function* () {
1079
+ yield inst;
1080
+ });
1081
+
1082
+ // key and schema snapshotted together: reading one live and the other cached lets a later mutation of the caller's shape object pair a stale key with a missing schema
1083
+ let entries!: [string | symbol, schemas.$ZodType][];
1084
+ inst._zod.check = (payload) => {
1085
+ // the base schema already typed the value, so only a nullish one is rejected here: the properties read on a primitive too, matching z.property() on a string's length
1086
+ if (payload.value == null) {
1087
+ payload.issues.push({ expected: "object", code: "invalid_type", input: payload.value, inst });
1088
+ return undefined;
1089
+ }
1090
+ entries ??= Reflect.ownKeys(def.shape).map((key) => [key, (def.shape as any)[key] as schemas.$ZodType]);
1091
+ const input = payload.value as any;
1092
+ let proms: Promise<any>[] | undefined;
1093
+ for (const [key, schema] of entries) {
1094
+ const result = schema._zod.run({ value: input[key], issues: [] }, {});
1095
+ if (result instanceof Promise) {
1096
+ proms ??= [];
1097
+ proms.push(result.then((result) => handleCheckPropertyResult(result, payload, key)));
1098
+ } else {
1099
+ handleCheckPropertyResult(result, payload, key);
1100
+ }
1101
+ }
1102
+ if (proms) return Promise.all(proms).then(() => undefined);
1103
+ return undefined;
1104
+ };
1105
+ }
1106
+ );
1107
+
1048
1108
  ///////////////////////////////////
1049
1109
  ///// $ZodCheckMimeType /////
1050
1110
  ///////////////////////////////////
@@ -18,7 +18,7 @@ import {
18
18
  urlHostnameOk,
19
19
  urlProtocolOk,
20
20
  } from "./schemas.js";
21
- import type { $ZodProperties, $ZodPropertiesDef, ParseContextInternal, ParsePayload, SomeType } from "./schemas.js";
21
+ import type { ParseContextInternal, ParsePayload, SomeType } from "./schemas.js";
22
22
  import * as util from "./util.js";
23
23
 
24
24
  /** @internal Sentinel the compiled fast path returns when validation fails. */
@@ -89,7 +89,7 @@ type SupportedCheck =
89
89
  | checks.$ZodCheckLengthEquals
90
90
  | checks.$ZodCheckStringFormat
91
91
  | checks.$ZodCheckProperty
92
- | $ZodProperties
92
+ | checks.$ZodCheckProperties
93
93
  | checks.$ZodCheckMimeType
94
94
  | checks.$ZodCheckOverwrite
95
95
  | { _zod: { def: { check: "custom"; fn?: (value: unknown) => boolean }; check?: (payload: unknown) => unknown } };
@@ -455,7 +455,7 @@ function generateChecks(doc: Doc, ctx: CompileContext, schema: SomeType, accesso
455
455
  generatePropertyCheck(doc, ctx, def, currentAccessor);
456
456
  break;
457
457
  case "properties":
458
- generatePropertiesChecks(doc, ctx, def, currentAccessor, false);
458
+ generatePropertiesChecks(doc, ctx, def, currentAccessor);
459
459
  break;
460
460
  case "overwrite": {
461
461
  // Overwrite transforms the value - create new variable for transformed result
@@ -616,20 +616,15 @@ function generateMimeTypeCheck(
616
616
  function generatePropertiesChecks(
617
617
  doc: Doc,
618
618
  ctx: CompileContext,
619
- def: $ZodPropertiesDef,
620
- accessor: string,
621
- schemaRole: boolean
619
+ def: checks.$ZodCheckPropertiesDef,
620
+ accessor: string
622
621
  ): void {
623
- // a custom `when` gates the assertion at runtime; inside a union a wrongly-run branch is absorbed as a branch failure rather than falling back, so refuse at codegen the way the check role does
622
+ // a custom `when` gates the assertion at runtime; inside a union a wrongly-run branch is absorbed as a branch failure rather than falling back, so refuse at codegen
624
623
  if (def.when) {
625
624
  throw new ZodCompileUnsupportedError(`check with a custom "when" condition`);
626
625
  }
627
- // matches the runtime gate for whichever role this is: a schema rejects a primitive outright, a check only a nullish value
628
- doc.write(
629
- schemaRole
630
- ? `if (${accessor} === null || (typeof ${accessor} !== "object" && typeof ${accessor} !== "function")) return INVALID;`
631
- : `if (${accessor} == null) return INVALID;`
632
- );
626
+ // matches the runtime gate: the base schema already typed the value, so only a nullish one is rejected
627
+ doc.write(`if (${accessor} == null) return INVALID;`);
633
628
  const shape = def.shape as Record<string | symbol, SomeType>;
634
629
  for (const key of Reflect.ownKeys(shape)) {
635
630
  // a symbol has no source literal, so it is hoisted as a constant
@@ -934,7 +929,6 @@ type SupportedSchemaType =
934
929
  | "lazy"
935
930
  | "pipe"
936
931
  | "custom"
937
- | "properties"
938
932
  | "transform"
939
933
  | "catch";
940
934
 
@@ -1079,10 +1073,6 @@ function generateCheck(
1079
1073
  case "custom":
1080
1074
  typeAccessor = generateCustomCheck(doc, ctx, schema, accessor);
1081
1075
  break;
1082
- case "properties":
1083
- generatePropertiesChecks(doc, ctx, (schema as $ZodProperties)._zod.def, accessor, true);
1084
- typeAccessor = accessor;
1085
- break;
1086
1076
  case "transform":
1087
1077
  typeAccessor = generateTransformCheck(doc, ctx, schema, accessor);
1088
1078
  break;
@@ -16,7 +16,6 @@ import {
16
16
  finalize,
17
17
  handleUnrepresentable,
18
18
  initializeContext,
19
- isTransforming,
20
19
  processSchema,
21
20
  } from "./to-json-schema.js";
22
21
  import { BIGINT_FORMAT_RANGES, NUMBER_FORMAT_RANGES, assignProp, getEnumValues } from "./util.js";
@@ -481,54 +480,6 @@ export const objectProcessor: Processor<schemas.$ZodObject> = (schema, ctx, _jso
481
480
  }
482
481
  };
483
482
 
484
- // asserts named properties in place and passes everything else through, so no additionalProperties constraint is emitted
485
- export const propertiesProcessor: Processor<schemas.$ZodProperties> = (schema, ctx, _json, params) => {
486
- const json = _json as JSONSchema.ObjectSchema;
487
- const def = schema._zod.def;
488
-
489
- // dropping a symbol key silently would emit a schema that asserts less than this one does
490
- if (
491
- Object.getOwnPropertySymbols(def.shape).length &&
492
- handleUnrepresentable(schema, ctx, json, params, "Symbol keys cannot be represented in JSON Schema")
493
- ) {
494
- return;
495
- }
496
-
497
- // the parsed value is the input, so an output-mode emission would describe a transformed value this schema never returns
498
- if (ctx.io === "output") {
499
- for (const key in def.shape) {
500
- if (
501
- isTransforming(def.shape[key]!) &&
502
- handleUnrepresentable(
503
- schema,
504
- ctx,
505
- json,
506
- params,
507
- `z.properties() returns its input, so the output of a transforming schema at key "${key}" cannot be represented in JSON Schema`
508
- )
509
- ) {
510
- return;
511
- }
512
- }
513
- }
514
-
515
- json.type = "object";
516
- json.properties = {};
517
- for (const key in def.shape) {
518
- assignProp(
519
- json.properties,
520
- key,
521
- processSchema(def.shape[key]!, ctx as any, {
522
- ...params,
523
- path: [...params.path, "properties", key],
524
- })
525
- );
526
- }
527
- // input-side optionality in both modes: the parsed value is the input, so a defaulted key that stays absent must not be required of the output either
528
- const required = Object.keys(def.shape).filter((key) => inputOptin(def.shape[key]!) === undefined);
529
- if (required.length > 0) json.required = required;
530
- };
531
-
532
483
  export const unionProcessor: Processor<schemas.$ZodUnion> = (schema, ctx, json, params) => {
533
484
  const def = schema._zod.def as schemas.$ZodUnionDef;
534
485
  // Exclusive unions (inclusive === false) use oneOf (exactly one match) instead of anyOf (one or more matches). This includes both z.xor() and discriminated unions
@@ -904,7 +855,6 @@ export const allProcessors: Record<string, Processor<any>> = {
904
855
  file: fileProcessor,
905
856
  success: successProcessor,
906
857
  custom: customProcessor,
907
- properties: propertiesProcessor,
908
858
  function: functionProcessor,
909
859
  transform: transformProcessor,
910
860
  map: mapProcessor,
@@ -28,9 +28,9 @@ type WithState = { [STATE]?: State };
28
28
 
29
29
  const NO_ISSUES: errors.$ZodRawIssue[] = [];
30
30
 
31
- // a value a cycle can close through; callables count, since z.properties asserts on one
31
+ // a value a cycle can close through
32
32
  function isRef(value: unknown): value is object {
33
- return value !== null && (typeof value === "object" || typeof value === "function");
33
+ return value !== null && typeof value === "object";
34
34
  }
35
35
 
36
36
  // Receivers prefix paths in place, so the cache and every hand-out need their own copies.
@@ -90,9 +90,6 @@ function isRecursive(inst: $ZodType, stack: Set<object>, resolve: boolean): Answ
90
90
  check(def.catchall);
91
91
  break;
92
92
  }
93
- case "properties":
94
- merge(shape(def.shape, false));
95
- break;
96
93
  case "array":
97
94
  check(def.element);
98
95
  break;