You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(v4): make z.properties() a check again (#6594)
Reverts the schema role that #6536 gave `z.properties()`. It is a check again, as in 4.5: it spreads into `.check()`, and `z.instanceof().properties()` stays as sugar over it. The standalone form is gone — `z.properties(shape).parse()`, `z.infer` of it, `z.toJSONSchema()` and `z.compile()` of it, and the cycle tracking only the schema role had.
The schema form was a trap with transforms. A transform inside the shape ran and was discarded, so the parsed value and the inferred type were both the input side, and writing the result back is not an option for the objects the feature exists for: assigning to a getter-only property such as `Response.status` throws. As a check, "the value is never changed" is what a check already means.
What stays: every failing key reports, symbol keys, the `params` argument, and compile support. The check now lives next to `$ZodCheckProperty` as `$ZodCheckProperties` and installs its `Symbol.iterator` as an own property, so the symbol-key loop in `util.members` and the memoizer's callable arm go with the schema role. The API reference drops the standalone paragraph; the 4.6 post already led with the method.
Bundle, gzipped from the built package: mini −17 / −16 / −16 on boolean / string / object, classic −35 / −38 / −44, the named import −447. Runtime and memory: the check body is the same code moved, construction pays one `Object.defineProperty` per `z.properties()` call, and each check instance carries one extra own property.
This removes an API that shipped in 4.6.0.
The underlying `z.properties()` is a standalone schema. It asserts the named properties in place and returns its input untouched — unlike`z.object()`, it never builds a new object, so prototypes, unlisted keys, and object identity survive. Schemas inside the shape are validated but their results are discarded, so a transform or a default never changes the parsed value.
2355
+
The check asserts each property in place. Unlike`z.object()`, it never builds a new object, so prototypes, unlisted keys, and object identity survive. Schemas inside the shape are validated but their results are discarded, so a transform or a default never changes the parsed value.
2356
2356
2357
-
```ts
2358
-
const Named =z.properties({ name: z.string() });
2359
-
2360
-
classDog {
2361
-
constructor(publicname:string) {}
2362
-
}
2363
-
const rex =newDog("Rex");
2364
-
Named.parse(rex); // => rex (the same instance)
2365
-
```
2366
-
2367
-
Spreading into `.check()` works too: `z.instanceof(Response).check(...z.properties({ ... }))`. As a check it asserts on whatever the base schema produced, so `z.string().check(...z.properties({ length: z.number().min(3) }))` reads a string's length; as a standalone schema it expects an object.
2357
+
The underlying `z.properties()` is a check, so it spreads into `.check()` too: `z.instanceof(Response).check(...z.properties({ ... }))`. It asserts on whatever the base schema produced, so `z.string().check(...z.properties({ length: z.number().min(3) }))` reads a string's length.
// Also carries the Standard Schema issue bag: a failing `~standard.validate` parses with a plain issue holder instead of constructing a ZodError. Measured 2974 / 3455 / 4540 locally plus 28 headroom.
43
43
// Also carries the lazy `safeParse` error: a getter and a setter on the failing result, and the async wrapper that keeps a sync throw a rejection. Measured 3021 / 3497 / 4581 locally plus 18 headroom.
44
44
// Also carries the symbol-key loop in `util.members`, which is what installs `$ZodProperties`'s `Symbol.iterator` on its prototype, and the memoizer's shared reference predicate. Measured 3036 / 3516 / 4611 locally plus 18 headroom.
45
-
"zod-mini-boolean": 3088,
45
+
// Lowered by the measured −17 / −16 / −16 from making z.properties() a check again: the symbol-key loop in `util.members` and the memoizer's callable arm left with the schema role. Measured 3044 / 3418 / 4629 locally plus 18 headroom; the object ceiling had been overtaken to zero headroom on main, so its number moves up by two while the bundle moves down.
46
+
"zod-mini-boolean": 3062,
46
47
// Also carries the code-point string length scan: `.min`/`.max`/`.length` on a string pulls in the surrogate walk.
47
48
// Also carries the Standard Schema issue bag: a failing `~standard.validate` parses with a plain issue holder instead of constructing a ZodError. Measured 2974 / 3455 / 4540 locally plus 28 headroom.
48
49
// Also carries the lazy `safeParse` error: a getter and a setter on the failing result, and the async wrapper that keeps a sync throw a rejection. Measured 3021 / 3497 / 4581 locally plus 18 headroom.
49
50
// Also carries the symbol-key loop in `util.members`, which is what installs `$ZodProperties`'s `Symbol.iterator` on its prototype, and the memoizer's shared reference predicate. Measured 3036 / 3516 / 4611 locally plus 18 headroom.
50
51
// Lowered by the measured −105 when checks stopped writing metadata into the bag at attach time: the string length/format closures and the per-instance bounded regex left every string bundle, and the template literal now derives its own part patterns.
51
-
"zod-mini-string": 3462,
52
+
// Lowered by the measured −17 / −16 / −16 from making z.properties() a check again: the symbol-key loop in `util.members` and the memoizer's callable arm left with the schema role. Measured 3044 / 3418 / 4629 locally plus 18 headroom; the object ceiling had been overtaken to zero headroom on main, so its number moves up by two while the bundle moves down.
53
+
"zod-mini-string": 3436,
52
54
// Also carries the construction-time discriminator check, which writes a WeakMap entry from `$ZodObject`, so every bundle containing `z.object` pays for it whether or not it builds a discriminated union.
53
55
// Also carries the declared symbol keys from #6448: `normalizeDef` collects the shape's own symbols and the parse loop walks them. Almost none of that is the `Reflect.ownKeys` conversions — reverting all eight of them measures a byte larger.
54
56
// Also carries the memoizer seam from #6482: each container init reads `globalConfig.memoizer` and calls `attach`, which is what lets `zod/mini` opt into cycle support. Measured 4512 locally but 4533 on CI — the gzip stream differs across zlib builds — so the headroom rides on the CI number.
// Also carries the symbol-key loop in `util.members`, which is what installs `$ZodProperties`'s `Symbol.iterator` on its prototype, and the memoizer's shared reference predicate. Measured 3036 / 3516 / 4611 locally plus 18 headroom.
58
60
// Also carries the guards `validate` parses under: an `aborted` check in each container loop, and two early exits in the generated object parser. Only a bundle containing a container pays — boolean is byte-identical to main and string measures three bytes smaller — so this ceiling rises by the measured +76 and the other two do not move. Measured 4696 locally plus 18 headroom.
59
61
// Lowered by the measured −69 for the same bag strip: the string and number inits no longer carry pattern derivation or attach-time metadata writes.
60
-
"zod-mini-object": 4645,
62
+
// Lowered by the measured −17 / −16 / −16 from making z.properties() a check again: the symbol-key loop in `util.members` and the memoizer's callable arm left with the schema role. Measured 3044 / 3418 / 4629 locally plus 18 headroom; the object ceiling had been overtaken to zero headroom on main, so its number moves up by two while the bundle moves down.
// `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",()=>{
// 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.
0 commit comments