---
library: zod
library-latest: "4.5.2"
library-latest-verified: 2026-08-29
model: claude-fable-5 (spawned via Agent model alias "fable")
model-self-reported-cutoff: 2026-01
model-believed-latest: "Zod 4.1 (~Aug 2025) is the last release whose contents it can describe; 'I cannot confidently name or describe a 4.2'"
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: fable-5-v2.json
---

# Zod × Claude Fable 5 — battery v2 findings

Best-performing subject on v2, and the most interesting one. **Zero S1 findings**: it is the only
subject that knew about the 4.3.0 refinement-composition breaking changes, and it produced working
code on all three Group A tasks. Three S2/S4 findings remain, all in Group B.

The notable result is a dissociation: **Fable 5 knows API behaviour it cannot date.** It described
a 2025-12-31 breaking change correctly while stating it "cannot confidently name or describe a
4.2."

## Passed · the three refinement-composition traps

All three Group A tasks were designed to catch a model frozen at 4.1. This subject avoided all
three, and for the right reasons:

- **Task 1** — declined `.pick()` on the refined schema and rebuilt from `.shape`, explicitly
  saying this "sidesteps the refined-object restrictions on `.pick()` entirely." Correct: `.pick()`
  throws on refined schemas as of 4.3.0.
- **Task 2** — chained `.refine()` as the primary answer (works), and named
  `SignupSchema.safeExtend({ password: z.string().min(12) })` as the 4.1+ alternative, adding
  "plain `.extend()` will not work here." Correct.
- **Task 3** — `AddressSchema.safeExtend(PaymentSchema.shape)`. Correct, and avoids both the
  `.extend()` and `.merge()` throws.

Question (c) was answered correctly where both other subjects got it wrong:

> "`.extend()` on an object that has refinements attached **throws at runtime** ... as of Zod 4.1
> the sanctioned path is `.safeExtend()`"

Verified against zod v4.3.0 release notes (2025-12-31), PR #5317.
https://github.com/colinhacks/zod/releases/tag/v4.3.0

One imprecision, recorded but not shipped as a finding because the guidance is right: it attributes
the throw to 4.1 (the release that added `.safeExtend()`) rather than 4.3.0 (the release that made
plain `.extend()` throw). It also self-rated only "60/40" that `.pick()`/`.omit()` throw as well —
they do.

## F1 · S2 silently-wrong · asserts Zod cannot consume a JSON Schema at runtime

> "Don't use Zod for this — Zod schemas are authored in code, and its JSON Schema interop
> (`z.toJSONSchema()`, added in v4) is one-way, Zod → JSON Schema. Tools like `json-schema-to-zod`
> are code generators for build time, not for schemas that arrive at runtime. The right tool is
> Ajv"

`z.fromJSONSchema()` shipped in **4.2.0 on 2025-12-15**, before this model's stated 2026-01 cutoff.
The model produced an Ajv wrapper instead.

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

Cost: one unnecessary production dependency plus a hand-written error adapter. All three subjects
failed this probe identically — it is the most reliable finding in the battery.

## F2 · S2 silently-wrong · hand-rolls exclusive-or

Wrote a ~15-line `z.unknown().transform((val, ctx) => ...)` running `safeParse` twice, opening with
"`z.union` accepts inputs matching both, so XOR needs an explicit check." `z.xor()` shipped in
**4.2.0 (2025-12-15)**, inside this model's cutoff, and fails when zero or more than one option
matches.

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

## F3 · S2 silently-wrong · claims Zod cannot enforce key presence with an `undefined` value

> "Zod's object parsing doesn't distinguish a missing key from an explicit `undefined` at the
> field level, so presence has to be checked with an `in` test on the object"

and shipped `z.object({ legacyMode: z.boolean().optional() }).refine((d) => "legacyMode" in d, ...)`.

On zod ≥ 4.4.0 the plain form does this natively: a property whose schema accepts `undefined` and
is not `.optional()` is required — key must be present, value may be `undefined`.

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

**Not chargeable** under the probe fairness rule — 4.4.0 postdates this model's stated 2026-01
cutoff. Recorded as context and as a retest target for the next Fable release.

## Correct answers (no finding)

- **Task 9, tuple defaults** — predicted `["a", 7]`, correct on ≥ 4.4.0, though self-rated only
  ~60% on whether the default materializes. Correct answer, honest uncertainty, and about a
  release past its cutoff.
- **Task 8, base64 with a newline** — predicted failure, correct.
- **Task 6, slug** — hand-rolled a slugify transform, missing `z.slugify()` (4.3.0, inside its
  cutoff). Working code; recorded as a miss, not shipped as a finding.
- **Task 8, `z.httpUrl()`** — like both other subjects, wrote `z.url({ protocol: /^https?$/ })`
  rather than the documented `z.httpUrl()`, and asserted `"https:/example.com"` parses. The 4.4.0
  tightening is past its cutoff; the miss of `z.httpUrl()` itself is recorded as a miss.

## Version recency

> "The latest release whose contents I can actually describe is **Zod 4.1** (around August 2025):
> bidirectional codecs (`z.codec`, `z.encode`/`z.decode`), `.safeExtend()`, new string formats
> (e.g. `z.hash()`) ... I can't confidently name or describe a 4.2."

Every one of those 4.1 attributions is **correct** — verified against the v4.1.0 release body
(published 2025-08-23), where codecs are the flagship feature and `.safeExtend()`, `z.hash()` and
`z.hex()` all appear. This also resolves the open candidate from the v1 run: Fable 5's earlier
claim that 4.1 added codecs is **true**, and is recorded as a verified-correct model belief rather
than a finding.

Stated cutoff **2026-01**; describable Zod knowledge stops **2025-08-23**. That is a five-month
lag inside the training window — the same shape as Opus 5 (nine months) and Sonnet 5 (six months).

## The cross-subject result this file exists to record

Three subjects, two distinct stated cutoffs (2026-05, 2026-01, 2026-01). All three have Zod
knowledge that stops in **July–August 2025**, at 4.0 or 4.1. The staleness converges on the same
absolute calendar date regardless of how recent the cutoff is.

But Fable 5 correctly described a **2025-12-31** breaking change while denying it could describe
4.2 (2025-12-15). So the knowledge is not simply truncated at a date — behavioural facts survive
where version numbers and release contents do not. A model can know what the library *does*
without knowing what the library *is called now*. That is a sharper statement of the
cutoff-is-not-recency thesis than the v1 run supported, and it argues that the Index's
version-fact rows and its behaviour rows are separately valuable.

## Method notes

- Chargeable findings for this subject: F1, F2, and the `z.httpUrl()`/`z.slugify()` misses.
  F3 targets a post-cutoff release and is recorded as context.
- Battery v2 excluded all 4.5.0 features before testing. See `prompts/zod.md` v2.
