---
library: zod
library-latest: "4.5.2"
library-latest-verified: 2026-08-29
model: claude-sonnet-5 (spawned via Agent model alias "sonnet")
model-self-reported-cutoff: 2026-01
model-believed-latest: "Zod 4 is current major; declines to name a minor. Last release whose contents it can describe: 4.0, believed ~May 2025"
test-date: 2026-08-29
battery: zod/v2 (10 idiomatic tasks + 3 direct questions; probes restricted to 4.2.0-4.4.3)
tool-uses-during-test: 0
verified-against: https://api.github.com/repos/colinhacks/zod/releases (release bodies + published_at) · https://zod.dev/api
status: open (no retest yet)
json: sonnet-5-v2.json
---

# Zod × Claude Sonnet 5 — battery v2 findings

Worst-performing subject on v2, and the one whose failures are hardest to notice. Seven findings.
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, and one full minor further back than Opus 5 or
Fable 5.

## F1 · S1 breaks-build · `.pick()` on a refined object schema throws

```ts
const EditProfileSchema = SignupSchema.pick({ email: true, displayName: true });
```

Presented 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. On zod ≥ 4.3.0 it
throws at construction.

Cite: zod v4.3.0 release notes (2025-12-31), PR #5317: "Using `.pick()` or `.omit()` on object
schemas with refinements now throws an error."
https://github.com/colinhacks/zod/releases/tag/v4.3.0

## F2 · S1 breaks-build · `.extend()` overwrite on a refined schema throws

```ts
const StrongPasswordSignupSchema = SignupSchema.extend({
  password: z.string().min(12, { error: "Password must be at least 12 characters" }),
});
```

This is the model's whole answer — no fallback, no base-schema refactor. It overwrites an existing
property on a schema carrying a refinement, which throws on zod ≥ 4.3.0. `.safeExtend()` (4.1.0)
is never mentioned. The model instead worried whether the refinement would "correctly re-validate
the *new* `password` field", a concern about behaviour the release replaced with an error.

Cite: zod v4.3.0 release notes, "overwriting properties with `.extend()` disallowed on object
schemas with refinements ... Instead you can use `.safeExtend()`".
https://github.com/colinhacks/zod/releases/tag/v4.3.0

## F3 · S2 silently-wrong · asserts `z.fromJSONSchema()` does not exist, with high confidence

> "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()`."

`z.fromJSONSchema()` shipped in **4.2.0 on 2025-12-15**, before this model's stated cutoff. The
model named the exact function and denied its existence. It then recommended `ajv` or the
third-party `json-schema-to-zod`.

Cite: zod v4.2.0 release notes, "Implement `z.fromJSONSchema()`".
https://github.com/colinhacks/zod/releases/tag/v4.2.0

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.

## F4 · S2 silently-wrong · hand-rolls exclusive-or, then advises abandoning schema-based validation

The model wrote a `z.custom<unknown>()` XOR guard that returns 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."

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

Cite: zod v4.3.0 release notes, "`z.xor()` — exclusive union" (landed 4.2.0, PR #5534).
https://github.com/colinhacks/zod/releases/tag/v4.3.0

## F5 · S2 silently-wrong · claims Zod cannot distinguish a missing key from an explicit `undefined`

> "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 then 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.

On zod ≥ 4.4.0, `z.object({ legacyMode: z.undefined() })` alone does exactly what was asked:
`safeParse({})` fails, `safeParse({ value: undefined })` succeeds.

Cite: zod v4.4.0 release notes (2026-04-29), "Required object properties with `z.undefined()`"
(PR #5661). https://github.com/colinhacks/zod/releases/tag/v4.4.0

Note: 4.4.0 postdates this model's stated 2026-01 cutoff, so this failure is **outside** the
probe fairness window and is recorded as context, not as a chargeable finding against Sonnet 5.
It is chargeable against Opus 5 (F6 in that file), whose cutoff is 2026-05.

## F6 · S2 silently-wrong · wrong runtime prediction for tuple defaults

> "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()`."

On zod ≥ 4.4.0, `z.tuple([z.string(), z.number().default(7)]).parse(["a"])` **succeeds and returns
`["a", 7]`** — tuple defaults now materialize. The model was the only subject to predict failure,
and it reasoned from the zod 3 implementation.

Cite: zod v4.4.0 release notes, "Tuple defaults now materialize output values correctly".
https://github.com/colinhacks/zod/releases/tag/v4.4.0

Same caveat as F5: 4.4.0 is past this model's stated cutoff, so this is context rather than a
chargeable finding. It is included because 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.

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

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**. Again outside its stated
cutoff and so recorded as context, but the miss of `z.httpUrl()` *itself* is not: the validator
predates 4.4.0 and is what the docs point at for this case.

Cite: https://zod.dev/api ("Use `z.httpUrl()`"); zod v4.4.0 release notes, "String validators are
stricter". https://github.com/colinhacks/zod/releases/tag/v4.4.0

## Correct answers (no finding)

- 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.
- Task 8, base64 with a newline: predicted failure — correct.
- Task 6, slug: hand-rolled slugify, missing `z.slugify()` (4.3.0, inside its cutoff). Working
  code; recorded as a miss, not shipped as a finding.

## 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**"

> "my stated knowledge cutoff is **January 2026**"

Truth: 4.0.0 shipped **2025-07-10**; current at test date is **4.5.2**. The model would not name a
current minor at all, which is the correct calibration move and is noted in its favour.

The lag here is **six months inside the training window** (Jul 2025 knowledge, Jan 2026 cutoff) —
and Sonnet 5 is one full minor behind Opus 5 and Fable 5, which both reach 4.1.

## Method notes

- Two of this subject's raw failures (F5, F6, and the 4.4.0 half of F7) target releases published
  after its stated 2026-01 cutoff. Under the probe fairness rule in `prompts/zod.md` v2 they are
  **not chargeable findings** — they are recorded because the dataset's value is longitudinal and
  because Sonnet 5's answers will become chargeable the moment a Sonnet with a later cutoff ships.
  Chargeable findings for this subject: F1, F2, F3, F4, and the `z.httpUrl()` miss in F7.
- Battery v2 excluded all 4.5.0 features before testing. See `prompts/zod.md` v2.
