fix(v4): apply key schema transforms in z.record()#5891
Conversation
z.record() with a finite-key keyType (literal, enum, etc.) skipped the keyType entirely on its fast path and used the original input key as the output property name. Any .transform() chained onto the key schema was therefore dropped at runtime, contradicting the inferred output type. z.partialRecord() already did the right thing because it clears the keyType's `values` to force the slow path. Run the keyType once per known input value to derive the output property name, so transforms apply. Required-key semantics (the reason the fast path exists) and unrecognized-key detection are preserved. Fixes #5296.
|
Reviewed PR #5891 — approved with no issues. The fix correctly applies key schema transforms in the known-values branch of Task list (4/4 completed)
|
|
TL;DR — Key changes
Summary | 2 files | 2 commits | base:
The fast path in
|
There was a problem hiding this comment.
Reviewed — no issues found.
The known-values branch was the only path that skipped running the key schema, so output keys were never transformed. The else branch (open key set, used by partialRecord) already used keyResult.value — this fix makes the two branches consistent. Error paths and unrecognized-key detection correctly continue to use the original input-side key. Tests pass (runtime + typecheck).
Task list (4/4 completed)
- Read the PR diff and understand the changes
- Read surrounding source code for context
- Verify correctness and edge cases
- Submit review
Claude Opus | 𝕏
When the keyType has a known value set, run any .refine()/.transform() errors through `invalid_key` rather than silently falling back to the input key. Matches the behavior of the non-exhaustive path.
|
Landed in Zod 4.4 |
Summary
z.record()with a finite-key keyType (literal, enum, union of literals, etc.) skipped the keyType entirely on its fast path and used the original input key as the output property name. Any.transform()chained onto the key schema was therefore dropped at runtime, contradicting the inferred output type.z.partialRecord()already did the right thing — it works by clearing the keyType'svaluesto force the slow path, where the keyType is parsed per input key.Approach
Run the keyType once per known input value to derive the output property name. The fast path's job — enforcing required-key semantics for finite-key records (
Record<K, V>vsPartial<Record<K, V>>) and detecting unrecognized keys — is preserved. Lookup into the input object still uses the input-side key.This is the same key-resolution work the slow path already does and that
partialRecordrelies on; the fast path was the lone code path skipping it.Behavior
z.record(z.literal("a").transform(() => "b"), v).parse({ a: 1 })→{ b: 1 }(was{ a: 1 }).z.record(z.literal(["a","b"]), v).parse({ a: 1 })— still fails on missing required keyb.z.record(z.literal(["a","b"]), v).parse({ a: 1, c: 2 })— still flagscas unrecognized.z.partialRecord(...)— unchanged.values/propValuespropagation).Fixes #5296. Supersedes #5867 (which added the same intent but without the underlying source change and bundled unrelated build-config edits).
Test plan
z.literal(...).transform(...), multi-value literal+transform, enum+transform, required-key semantics, andpartialRecordparity.