What Claude Sonnet 5 gets wrong about zod — battery v2, tested 2026-08-29

Run zod--claude-sonnet-5--v2--2026-08-29

Supersedes zod--claude-sonnet-5--v1--2026-08-29.

Summary

Worst-performing subject on v2, and the one whose failures are hardest to notice. Seven findings, five chargeable. Its Zod knowledge stops at 4.0 (2025-07-10) against a stated cutoff of 2026-01 — a six-month lag inside its own training window, one full minor further back than Opus 5 or Fable 5.

SubjectClaude Sonnet 5 claude-sonnet-5, Anthropic
Invoked asAgent tool, model alias "sonnet"
Cutoff the model states2026-01
Newest zod release it could place4.0.0 · 2025-07-10 (~6 month lag)
Oldest zod release it could not place4.1.0 · 2025-08-23 (so this run brackets the subject’s boundary to 2025-07-10 – 2025-08-23)
In its own wordsZod 4 is current major; declines to name a minor. Last release whose contents it can describe: 4.0, believed ~May 2025
Library at test timezod 4.5.2 (npm), verified 2026-08-29
Batteryzod/v2 · 10 tasks, 3 direct questions · probe window 4.2.0 to 4.4.3
Tool uses during test0 (a run with any tool use is void — we measure training knowledge, not retrieval)
Tested2026-08-29
Findings7, of which 5 chargeable

Findings

F1 · .pick() on a refined object schema throws

S1breaks-build · .pick() / .omit() · now-throws · changed in zod 4.3.0 (2025-12-31) · chargeable

What the model believes

Presented .pick() on a refined schema as the answer, with the caveat aimed at the wrong risk — it worried about whether a carried-over refinement would run vacuously, not about the call throwing.

What it wrote
const EditProfileSchema = SignupSchema.pick({ email: true, displayName: true });
What works on zod 4.5.2
const EditProfileSchema = z.object(SignupSchema.shape).pick({ email: true, displayName: true });
Impact

Throws at construction on zod >= 4.3.0.

Verified against

F2 · .extend() overwrite on a refined schema throws

S1breaks-build · .extend() / .safeExtend() · now-throws · changed in zod 4.3.0 (2025-12-31) · chargeable

What the model believes

This is the model's whole answer — no fallback, no base-schema refactor. It instead worried whether the refinement would "correctly re-validate the new password field", a concern about behaviour the release replaced with an error. .safeExtend() (4.1.0) is never mentioned.

What it wrote
const StrongPasswordSignupSchema = SignupSchema.extend({
  password: z.string().min(12, { error: "Password must be at least 12 characters" }),
});
What works on zod 4.5.2
const StrongPasswordSignupSchema = SignupSchema.safeExtend({
  password: z.string().min(12, { error: "Password must be at least 12 characters" }),
});
Impact

Throws on zod >= 4.3.0. Worse than the Opus 5 equivalent because there is no working primary answer alongside it.

Verified against

F3 · asserts z.fromJSONSchema() does not exist, with high confidence

S2silently-wrong · z.fromJSONSchema() · added · changed in zod 4.2.0 (2025-12-15) · chargeable

What the model believes

"Zod does not support this natively, and I'm confident about that. Zod 4 added z.toJSONSchema(mySchema) to go Zod → JSON Schema, not the reverse. There's no built-in z.fromJSONSchema()." It named the exact function and denied its existence, then recommended ajv or the third-party json-schema-to-zod.

What it wrote
// recommends ajv or json-schema-to-zod
What works on zod 4.5.2
z.fromJSONSchema(jsonSchemaDocument)
Impact

The explicit confidence marker is what makes this an S2 rather than an S4: a hedge would have sent the user to the changelog. "I'm confident about that" sends them to npm to install Ajv.

Verified against

F4 · hand-rolls exclusive-or, then advises abandoning schema-based validation

S2silently-wrong · z.xor() · added · changed in zod 4.2.0 (2025-12-15) · chargeable

What the model believes

Wrote a z.custom<unknown>() XOR guard returning only a boolean, noted it loses the "which branch matched" information, and concluded: "better to do the two safeParse calls yourself in a function and return the successful branch ... or build a small manual parser function instead of trying to force it into one declarative schema."

What it wrote
// z.custom<unknown>() XOR guard, then advice to drop the declarative approach
What works on zod 4.5.2
z.xor([A, B])
Impact

The stale prior does not just cost lines here; it talks the user out of the declarative approach entirely. z.xor() does this in one call and produces oneOf in JSON Schema output.

Verified against

F5 · claims Zod cannot distinguish a missing key from an explicit undefined

S2silently-wrong · z.undefined() object properties · behavior-changed · changed in zod 4.4.0 (2026-04-29) · recorded, not chargeable

4.4.0 postdates this subject's stated 2026-01 cutoff, so this is outside the probe fairness window and is recorded as context, not charged. It IS chargeable against Opus 5 (F6 of that run), whose cutoff is 2026-05.

What the model believes

"This is a genuine gap in plain Zod object parsing ... a normal z.object({ legacyMode: z.undefined() }) cannot distinguish 'key present with value undefined' from 'key missing entirely'". It shipped a 10-line z.preprocess guard with an in check, and flagged uncertainty about the z.preprocess callback signature — uncertainty about a workaround for a problem that no longer exists.

What it wrote
// 10-line z.preprocess guard with an `in` check
What works on zod 4.5.2
z.object({ legacyMode: z.undefined() })
Impact

On zod >= 4.4.0 the plain form does exactly what was asked: safeParse({}) fails, safeParse({ legacyMode: undefined }) succeeds.

Verified against

F6 · wrong runtime prediction for tuple defaults

S2silently-wrong · z.tuple() defaults · behavior-changed · changed in zod 4.4.0 (2026-04-29) · recorded, not chargeable

Same as F5 — 4.4.0 postdates the stated 2026-01 cutoff. Recorded as context and as a retest target.

What the model believes

"My best answer: it fails. Zod's tuple parser checks the input array's length against the tuple's declared arity before it gets to applying per-element logic like .default()." It reasoned from the zod 3 implementation, and was the only subject to predict failure.

What works on zod 4.5.2
z.tuple([z.string(), z.number().default(7)]).parse(["a"]) // ["a", 7]
Impact

The direction of the error matters: the model predicted a hard failure where the library now silently supplies a value, which is the failure shape most likely to produce a wrong bug report.

Verified against

F7 · misses z.httpUrl(), frames the leniency as a security problem to hand-roll around

S2silently-wrong · z.httpUrl() · stricter · changed in zod 4.4.0 (2026-04-29) · chargeable

Split finding. The 4.4.0 strictness half is outside this subject's cutoff and is context only. The miss of z.httpUrl() ITSELF is chargeable: the validator predates 4.4.0 and is what the docs point at for this case.

What the model believes

Wrote z.url({ protocol: /^https?$/ }), asserted "https:/example.com" parses successfully, and escalated: "This exact leniency has been the root cause of real SSRF/URL-confusion vulnerabilities" — without knowing that z.httpUrl(), the documented built-in for http/https URLs, which also validates the hostname as a domain, rejects that input as of 4.4.0.

What it wrote
z.url({ protocol: /^https?$/ })
What works on zod 4.5.2
z.httpUrl()
Impact

Raises a security alarm and prescribes a hand-rolled fix for a case the library already covers with a purpose-built validator.

Verified against

What it got right, and near misses

Recorded so the run cannot be read as a hit list. A model that is right for an obsolete reason is recorded here, not as a finding.

KindAPINote
correct.merge() Task 3: z.intersection(AddressSchema, PaymentSchema) — works, and the model's stated reason (robust to whatever the inputs are) is sound. Avoided the 4.4.0 .merge() throw.
correctz.base64() Task 8, base64 with a newline: predicted failure — correct.
missz.slugify() Task 6, slug: hand-rolled slugify, missing z.slugify() (4.3.0, inside its cutoff). Working code. (Working code; recorded as a miss rather than shipped as a finding.)
context Version recency: "the most recent Zod release whose actual contents I can describe with real confidence is Zod 4.0, the initial stable v4 release ... My best guess is that shipped around May 2025" against a stated cutoff of January 2026. 4.0.0 in fact shipped 2025-07-10. The model would not name a current minor at all, which is the correct calibration move and is noted in its favour. (The version-recency finding for this subject is F3 of its v1 run; repeated here as the measurement that establishes the six-month lag.)

Sources

Battery specification: prompts/zod.md in the studio repo. Every finding above also carries its own citation.