Commit 02c2baf
authored
Make z.preprocess defer optionality to inner schema (#5929)
* Make z.preprocess defer optionality to inner schema (#5917)
`z.preprocess(fn, schema)` desugared to `pipe(transform(fn), schema)`,
so the resulting pipe's `optin` was inherited from `ZodTransform`
(undefined) instead of from the inner schema. When the inner schema was
itself optional and the preprocess sat as an object property, the
object compiler took the `!isOptionalIn` branch and synthesized a
`nonoptional` issue for missing keys — making the position of
`.optional()` change presence semantics.
Introduce `ZodPreprocess` as a structural subtype of `ZodPipe` (same
`def.type === "pipe"`, `instanceof ZodPipe` still true). It pins
`def.in` to a permissive `z.unknown()` and re-installs the four lazy
metadata props so `optin`, `optout`, `values`, and `propValues` all
defer to `def.out` (the inner schema). Backward direction throws,
matching today's behavior.
The two JSON-schema sites that special-cased the old
`pipe(transform, ...)` shape (`pipeProcessor` and `isTransforming`)
now also detect preprocess via `_zod.traits`.
* Stop deferring values/propValues from inner schema
`values` and `propValues` describe the *input* set a schema accepts.
Preprocess opens that set to anything `fn` can map to a B-accepted
value, so deferring these to B was unsound:
- discriminated unions use `propValues` as a fast-path disc map; with
B's propValues exposed, an option claims to match only B's literal
inputs and silently routes wrong (e.g. preprocess(toUpperCase,
z.literal("A")) would fail to match input "a"). Now reverts to
throwing at construction, matching pre-PR behavior.
- `z.record()` uses `values` to enumerate expected keys; with B's
values exposed it short-circuits before the preprocess fn ever runs
on input keys.
Presence semantics (`optin`/`optout`) describe whether the *outer*
container can omit this slot; preprocess is transparent to those, so
they continue to defer to B (the original #5917 fix).
* Add type-level tests for ZodPreprocess assignability
Document the invariant that ZodPreprocess<B> is structurally
assignable to ZodPipe<$ZodType, B>, and that the optin/optout
override surfaces B's declared type (narrowed for schemas like
ZodOptional that hardcode it, open `"optional" | undefined` for
schemas like ZodString that inherit it).
* Use traits.has consistently in pipeProcessor
`pipeProcessor` was mixing detection styles: `traits.has("$ZodPreprocess")`
for the new subtype and `def.in._zod.def.type === "transform"` for the
legacy pipe(transform, ...) form. Both check "input contributes no
validation, use B for input-side JSON schema." Converge on traits.
* Treat ZodCodec as transforming for JSON schema example/default stripping
Same diagnosis as the preprocess case: `isTransforming` recurses into
`def.in` and `def.out` looking for `def.type === "transform"`, but
`ZodCodec` embeds an implicit transform fn (its `decode`/`encode`)
while `def.in` and `def.out` are validating schemas. The recursion
finds no transform and returns false even though the codec absolutely
is transforming, causing output-side examples/defaults to leak into
the input JSON schema.
Detect via `_zod.traits.has("$ZodCodec")` alongside the preprocess
check.
* Document ZodPreprocess in inheritance hierarchy
Surface ZodPreprocess everywhere the codebase enumerates first-party
types as a structural sibling of ZodCodec under ZodPipe:
- core.mdx: $ZodTypes union comment ("$ZodCodec and $ZodPreprocess
extend this") and the inheritance diagram
- assignability.test.ts: explicit `satisfies` checks confirming the
type-level relationship to $ZodPreprocess and $ZodPipe (with
generics specified — the bare-generic form bites variance because
inherited reverseTransform is contravariant in B)
The $ZodTypes union itself doesn't need a new entry — preprocess is
covered transitively via $ZodPipe, same as $ZodCodec — but documenting
the relationship in prose and tests makes the hierarchy discoverable.
* Simplify ZodPreprocess to a pure metadata-override subtype
The bug is purely a static-metadata problem: $ZodPipeInternals.optin
was inheriting from A (the leading transform, which has no optin)
instead of from B (the inner schema). The runtime parse path for
pipe(transform(fn), schema) was already correct.
So the simpler design is: keep the runtime structure exactly as the
legacy form (def.in is a real ZodTransform), and just override the
optin/optout lazies in a thin subtype.
What this eliminates from the previous design:
- Custom parse function in $ZodPreprocess.init (forward direction with
embedded transform fn)
- Custom parse function in classic ZodPreprocess.init (addIssue
injection — the inner ZodTransform's classic parse override already
does this)
- The synthetic z.unknown() input slot
- The required `transform` field on the def
- traits.has("$ZodPreprocess") check in pipeProcessor and
isTransforming — def.in is a real ZodTransform, so the existing
detection paths fire correctly
The traits.has("$ZodCodec") check in isTransforming stays — codec
embeds its transform fn directly on the def (different shape) and
needs its own detection.
* Trim verbose comments to match house style1 parent 8ec4e73 commit 02c2baf
9 files changed
Lines changed: 156 additions & 4 deletions
File tree
- packages
- docs/content/packages
- zod/src/v4
- classic
- tests
- core
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
84 | 84 | | |
85 | 85 | | |
86 | 86 | | |
87 | | - | |
| 87 | + | |
88 | 88 | | |
89 | 89 | | |
90 | 90 | | |
| |||
156 | 156 | | |
157 | 157 | | |
158 | 158 | | |
| 159 | + | |
159 | 160 | | |
160 | 161 | | |
161 | 162 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2361 | 2361 | | |
2362 | 2362 | | |
2363 | 2363 | | |
| 2364 | + | |
| 2365 | + | |
| 2366 | + | |
| 2367 | + | |
| 2368 | + | |
| 2369 | + | |
| 2370 | + | |
| 2371 | + | |
| 2372 | + | |
| 2373 | + | |
| 2374 | + | |
| 2375 | + | |
| 2376 | + | |
| 2377 | + | |
| 2378 | + | |
| 2379 | + | |
2364 | 2380 | | |
2365 | 2381 | | |
2366 | 2382 | | |
| |||
2645 | 2661 | | |
2646 | 2662 | | |
2647 | 2663 | | |
2648 | | - | |
2649 | | - | |
| 2664 | + | |
| 2665 | + | |
| 2666 | + | |
| 2667 | + | |
| 2668 | + | |
| 2669 | + | |
2650 | 2670 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
143 | 143 | | |
144 | 144 | | |
145 | 145 | | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
146 | 152 | | |
147 | 153 | | |
148 | 154 | | |
| |||
Lines changed: 26 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
280 | 280 | | |
281 | 281 | | |
282 | 282 | | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2896 | 2896 | | |
2897 | 2897 | | |
2898 | 2898 | | |
| 2899 | + | |
| 2900 | + | |
| 2901 | + | |
| 2902 | + | |
| 2903 | + | |
| 2904 | + | |
| 2905 | + | |
| 2906 | + | |
| 2907 | + | |
| 2908 | + | |
| 2909 | + | |
| 2910 | + | |
| 2911 | + | |
| 2912 | + | |
| 2913 | + | |
| 2914 | + | |
| 2915 | + | |
| 2916 | + | |
| 2917 | + | |
| 2918 | + | |
| 2919 | + | |
| 2920 | + | |
2899 | 2921 | | |
2900 | 2922 | | |
2901 | 2923 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
525 | 525 | | |
526 | 526 | | |
527 | 527 | | |
528 | | - | |
| 528 | + | |
| 529 | + | |
529 | 530 | | |
530 | 531 | | |
531 | 532 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4118 | 4118 | | |
4119 | 4119 | | |
4120 | 4120 | | |
| 4121 | + | |
| 4122 | + | |
| 4123 | + | |
| 4124 | + | |
| 4125 | + | |
| 4126 | + | |
| 4127 | + | |
| 4128 | + | |
| 4129 | + | |
| 4130 | + | |
| 4131 | + | |
| 4132 | + | |
| 4133 | + | |
| 4134 | + | |
| 4135 | + | |
| 4136 | + | |
| 4137 | + | |
| 4138 | + | |
| 4139 | + | |
| 4140 | + | |
| 4141 | + | |
| 4142 | + | |
| 4143 | + | |
| 4144 | + | |
| 4145 | + | |
| 4146 | + | |
| 4147 | + | |
| 4148 | + | |
| 4149 | + | |
| 4150 | + | |
| 4151 | + | |
4121 | 4152 | | |
4122 | 4153 | | |
4123 | 4154 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
561 | 561 | | |
562 | 562 | | |
563 | 563 | | |
| 564 | + | |
564 | 565 | | |
565 | 566 | | |
566 | 567 | | |
| |||
0 commit comments