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
{{ message }}
Repository navigation
.readonly() freezes the input on a failed parse, and safeParse throws for typed arrays #6745
$ZodReadonly calls Object.freeze(payload.value) even when the inner schema failed, so safeParse freezes the caller's rejected object, and throws instead of returning an error when the rejected value is a non-empty typed array or Buffer.
Confirmed on main (19c40d2). Skipping the freeze on failure, like you suggested, fixes both cases:
function handleReadonlyResult(payload: ParsePayload): ParsePayload {
// A repeat visit hands back a node that is still being built; freezing it here would make the rest of its keys fail to assign.
- if (!payload.memo) payload.value = Object.freeze(payload.value);+ // A failed parse hands back the caller's input, which must not be frozen (and typed arrays cannot be).+ if (!payload.memo && payload.issues.length === 0) payload.value = Object.freeze(payload.value);
return payload;
}
Before → after:
string().readonly() on object: false, input frozen: true → false, input frozen: false
array().readonly() on Uint8Array: THROWS "Cannot freeze..." → success false, invalid_type
success path still frozen: true → true
The async path (.refine(async ...) + safeParseAsync) also leaves the input unfrozen, since it goes through the same helper.
Regression test added to packages/zod/src/v4/classic/tests/readonly.test.ts. It covers sync object, async object and Uint8Array, and fails on main in both the zod and compile-mode projects.
nub run test packages/zod/src/v4: 411 files, 6799 tests passed, no type errors
Activity
hamarshehmhmd commentedon Nov 7, 2026
Confirmed on main (19c40d2). Skipping the freeze on failure, like you suggested, fixes both cases:
function handleReadonlyResult(payload: ParsePayload): ParsePayload { // A repeat visit hands back a node that is still being built; freezing it here would make the rest of its keys fail to assign. - if (!payload.memo) payload.value = Object.freeze(payload.value); + // A failed parse hands back the caller's input, which must not be frozen (and typed arrays cannot be). + if (!payload.memo && payload.issues.length === 0) payload.value = Object.freeze(payload.value); return payload; }Before → after:
The async path (
.refine(async ...)+safeParseAsync) also leaves the input unfrozen, since it goes through the same helper.Regression test added to
packages/zod/src/v4/classic/tests/readonly.test.ts. It covers sync object, async object andUint8Array, and fails on main in both thezodandcompile-modeprojects.nub run test packages/zod/src/v4: 411 files, 6799 tests passed, no type errorsHappy to open a PR.