---
library: zod
library-latest: "4.5.2"
library-latest-verified: 2026-08-29
model: claude-opus-5 (spawned via Agent model alias "opus")
model-self-reported-cutoff: 2026-05
model-believed-latest: "cannot describe any release after the 4.1 line (~Aug 2025); declines to name a current version"
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: opus-5-v2.json
---

# Zod × Claude Opus 5 — battery v2 findings

Battery v1 (2026-08-28) produced zero S1/S2/S3 for this subject. v2 targets the window between
where its Zod knowledge stops (~4.1, 2025-08-23) and its stated cutoff (2026-05). Six findings,
including two that break at construction time.

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

Asked to derive an `EditProfileSchema` from a signup schema carrying a password-confirmation
`.refine()`, the model wrote:

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

and asserted it is "correct and safe *here*". On zod ≥ 4.3.0 this **throws at construction**.

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

The model stated the pre-4.3 behaviour as current fact: "the derived schema does not carry the
refinement over ... This is silent: no error, no warning." That was true through 4.2 and is the
exact behaviour 4.3.0 replaced with a throw. The upstream migration path
(`z.object(schema.shape).pick({...})`) was not mentioned.

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

The model's primary answer to "tighten the password rule" was a refactor to an unrefined base
schema, which works. Its no-refactor fallback was:

> `SignupSchema.extend({ password: z.string().min(12) }).refine(passwordsMatch, passwordsMatchParams)`

On zod ≥ 4.3.0 the `.extend()` call throws, because it overwrites an existing property on a
schema with refinements. The sanctioned API is `.safeExtend()`, which the model never named
anywhere in the battery despite it shipping in 4.1.0 — the same release whose contents the model
says is the last it can describe.

Cite: zod v4.3.0 release notes, "overwriting properties with `.extend()` disallowed on object
schemas with refinements": "`schema.extend({ a: z.number() }); // 4.3: throws error`. Instead you
can use `.safeExtend()`". https://github.com/colinhacks/zod/releases/tag/v4.3.0

The model also labelled plain `.extend()` "a bug" for the wrong reason — silent refinement loss,
not a throw. Right verdict, obsolete mechanism.

## F3 · S2 silently-wrong · declares an unnecessary dependency for JSON Schema input

Asked to validate against a JSON Schema document received at runtime, the model answered:

> "**Zod can't do this.** Zod 4 ships `z.toJSONSchema()` — Zod → JSON Schema — but there is no
> built-in JSON Schema → Zod direction, and there can't really be a runtime one"

and produced ~20 lines wrapping `ajv` + `ajv-formats` behind `z.unknown().superRefine()`.

`z.fromJSONSchema()` has existed since **4.2.0, published 2025-12-15** — six months before this
model's stated cutoff.

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

Cost of the stale prior: two new production dependencies and a hand-written error adapter, in
place of one built-in call. This is the highest-cost finding in the battery — a confidently
stated false negative ("can't do this", "there can't really be") that sends the user out of the
library.

## F4 · S2 silently-wrong · hand-rolls exclusive-or, states Zod cannot express it

> "`z.union` is inclusive-or, and `z.discriminatedUnion` needs a shared literal key, so neither
> expresses 'not both'."

The model then wrote a ~14-line `z.unknown().superRefine(...).pipe(z.union([A, B]))` construction
running `safeParse` twice. `z.xor()` — an exclusive union that fails when zero or more than one
option matches — shipped in **4.2.0 (2025-12-15)**.

Cite: zod v4.3.0 release notes, "`z.xor()` — exclusive union ... Unlike `z.union()` which passes
if *any* option matches, `z.xor()` fails if zero or more than one option matches." (feature landed
in 4.2.0 via PR #5534). https://github.com/colinhacks/zod/releases/tag/v4.3.0

## F5 · S2 silently-wrong · misses `z.httpUrl()`, prescribes a manual normalization workaround

For a webhook `targetUrl` restricted to http/https the model wrote
`z.url({ protocol: /^https?$/ })`, and then asserted:

> "**Does `"https:/example.com"` parse? Yes, it succeeds.** ... you cannot use `z.url()` to reject
> sloppy single-slash input"

followed by a hand-written `.transform((s) => new URL(s).toString())` normalization step.

`z.httpUrl()` is the documented built-in for exactly this case ("In many cases, you'll want to
validate Web URLs specifically. Use `z.httpUrl()`" — https://zod.dev/api), and since **4.4.0
(2026-04-29)** it rejects precisely the input the model says cannot be rejected:

```ts
z.httpUrl().safeParse("https:/example.com").success; // false
```

Cite: zod v4.4.0 release notes, "String validators are stricter": "HTTP URL validation through
`z.httpUrl()` now rejects malformed HTTP(S) URLs with a missing slash after the protocol."
https://github.com/colinhacks/zod/releases/tag/v4.4.0

Scoped claim: we did **not** verify whether `z.url({ protocol })` also rejects the single-slash
form, so the finding is that the model neither reached for the purpose-built validator nor knew
that the leniency it described as unavoidable had been fixed inside its own training window.
This one is security-adjacent — the field is a webhook target.

## F6 · S4 wrong-metadata · correct code, disbelieved

For "key must be present, value may be `undefined`" the model wrote
`z.union([z.boolean(), z.undefined()])` — which is **correct** on zod ≥ 4.4.0, where a property
whose schema accepts `undefined` and is not `.optional()` is required. It then disclaimed its own
answer ("I'm less sure Zod enforces key *presence* at runtime here") and offered an 8-line
`z.unknown().superRefine(...).pipe(...)` workaround as the safe path.

Cite: zod v4.4.0 release notes, "Required object properties with `z.undefined()`" (PR #5661):
"A property whose schema is `z.undefined()` is now treated as required. The key must be present,
but its value may be `undefined`." https://github.com/colinhacks/zod/releases/tag/v4.4.0

The behaviour was pinned down 2026-04-29, four weeks before the stated cutoff. Output is correct;
the belief is not, and the belief is what a user acts on.

## Correct answers (no finding)

- Task 3, combining refined schemas: avoided `.merge()` entirely (shape-hoist + re-refine, or
  `.and()`). Would not have hit the 4.4.0 `.merge()`-throws change. The `.merge()` deprecation is
  old enough to be solidly in training.
- Task 9, tuple defaults: predicted `T.parse(["a"])` returns `["a", 7]` — **correct** on ≥ 4.4.0,
  and it flagged that zod 3 fails. Confidence self-rated "moderately high". The only subject to
  get this right with a confident reason.
- Task 8, base64 with a newline: predicted failure — correct today. Its stated reasoning (the
  regex was always anchored) implies it believes base64 was always strict; in fact whitespace was
  accepted until 4.4.0. Right answer, obsolete model of why. Not shipped as a finding: no wrong
  output.
- Task 6, slug: hand-rolled a slugify transform rather than using `z.slugify()` (4.3.0). Working
  code, defensible choice, recorded as a miss but not shipped as a finding.

## Version recency (repeat of v1 F1, reproduced verbatim)

> "The most recent Zod release whose *contents* I can actually describe is the **4.1 line,
> released around August 2025** ... I cannot describe the contents of any release after 4.1 and
> would not trust myself to name the current version."

Stated cutoff **2026-05**. Zod knowledge stops **2025-08-23** (4.1.0). Truth at test date:
4.5.2. Same answer as the v1 run 24 hours earlier — the lag is stable and reproducible, not
sampling noise. Calibration remains excellent: it named the blind spot unprompted and told the
reader to check the changelog.

## Method notes

- Self-test: the operator model testing itself. Recorded transparently and read as weaker than an
  independent test. No finding here rests on self-assessment — each is a diff against a fetched
  primary source.
- Five candidate probes from the v2 sketch (`z.compile`, `z.validate`, `z.creditCard`,
  `z.properties`, `deepPartial`/`exactPartial`) were **excluded** before testing: all shipped in
  4.5.0 on 2026-08-28, after every subject's cutoff. See `prompts/zod.md` v2, probe fairness rule.
- In particular, this model's v1 claim that `.deepPartial()` was removed is **not** a finding.
  It was true at its cutoff; the functional form returned in 4.5.0, one day before the test.
