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

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

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

Summary

Best-performing subject on v2 and the most interesting one. Zero S1 findings: the only subject that knew about the 4.3.0 refinement-composition breaking changes, producing working code on all three Group A traps. The notable result is a dissociation — it described a 2025-12-31 breaking change correctly while stating it "cannot confidently name or describe a 4.2" (2025-12-15). Knowledge is not simply truncated at a date: behavioural facts survive where version numbers and release contents do not.

SubjectClaude Fable 5 claude-fable-5, Anthropic
Invoked asAgent tool, model alias "fable"
Cutoff the model states2026-01
Newest zod release it could place4.1.0 · 2025-08-23 (~5 month lag)
Oldest zod release it could not place4.2.0 · 2025-12-15 (so this run brackets the subject’s boundary to 2025-08-23 – 2025-12-15)
In its own wordsZod 4.1 (~Aug 2025) is the last release whose contents it can describe; 'I cannot confidently name or describe a 4.2'
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
Findings3, of which 2 chargeable

Findings

F1 · asserts Zod cannot consume a JSON Schema at runtime

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

What the model believes

"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". The model produced an Ajv wrapper instead.

What it wrote
// Ajv wrapper
What works on zod 4.5.2
z.fromJSONSchema(jsonSchemaDocument)
Impact

One unnecessary production dependency plus a hand-written error adapter. All three v2 subjects failed this probe identically — the most reliable finding in the battery.

Verified against

F2 · hand-rolls exclusive-or

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

What the model believes

Opened with "z.union accepts inputs matching both, so XOR needs an explicit check", then wrote a ~15-line z.unknown().transform((val, ctx) => ...) running safeParse twice.

What it wrote
// ~15-line z.unknown().transform((val, ctx) => ...) running safeParse twice
What works on zod 4.5.2
z.xor([A, B])
Impact

~15 lines and a double parse in place of one call.

Verified against

F3 · claims Zod cannot enforce key presence with an undefined value

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. Recorded as context and as a retest target for the next Fable release.

What the model believes

"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", shipping z.object({ legacyMode: z.boolean().optional() }).refine((d) => "legacyMode" in d, ...).

What it wrote
z.object({ legacyMode: z.boolean().optional() }).refine((d) => "legacyMode" in d, ...)
What works on zod 4.5.2
z.object({ legacyMode: z.undefined() })
Impact

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.

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.pick() 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.
correct.safeExtend() 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.
correct.safeExtend() Task 3 — AddressSchema.safeExtend(PaymentSchema.shape). Correct, and avoids both the .extend() and .merge() throws.
correct.extend() Direct question (c), 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 the v4.3.0 release notes (2025-12-31), PR #5317.
imprecision.extend() It attributes the .extend() throw to 4.1 (the release that added .safeExtend()) rather than 4.3.0 (the release that made plain .extend() throw), and self-rated only "60/40" that .pick()/.omit() throw as well — they do. (The guidance it gives is right; only the attribution is off.)
correctz.tuple() defaults 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.
correctz.base64() Task 8, base64 with a newline — predicted failure, correct.
missz.slugify() Task 6, slug — hand-rolled a slugify transform, missing z.slugify() (4.3.0, inside its cutoff). Working code. (Working code, so not shipped as an F-numbered finding. The write-up's method notes nonetheless count this miss as chargeable for this subject, since 4.3.0 is inside its stated cutoff.) [chargeable miss — working code the library now supersedes — no severity level fits it; absent from the finding count]
missz.httpUrl() Task 8 — 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 inside it. (Not shipped as an F-numbered finding because the 4.4.0 strictness claim is out of window; the write-up's method notes count the z.httpUrl() miss itself as chargeable.) [chargeable miss — the arm licensed to charge states a cutoff below the release under test; absent from the finding count]
contextz.codec() 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. Stated cutoff 2026-01; describable Zod knowledge stops 2025-08-23 — a five-month lag inside the training window. (The model's 4.1 knowledge is accurate. This is a verified-true model belief, recorded as such.)
context The cross-subject result this run exists to record: three subjects, two distinct stated cutoffs (2026-05, 2026-01, 2026-01), all with Zod knowledge stopping in July–August 2025 at 4.0 or 4.1. Staleness converges on the same absolute calendar date regardless of how recent the cutoff is. But this subject correctly described a 2025-12-31 breaking change while denying it could describe 4.2 (2025-12-15) — so knowledge is not simply truncated at a date. A model can know what the library does without knowing what the library is called now, which argues that the Index's version-fact rows and its behaviour rows are separately valuable. (A cross-run measurement, not a per-run failure.)

Sources

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