What Claude Fable 5 gets wrong about zod — battery v4-a, tested 2026-09-02
Run zod--claude-fable-5--v4-a--2026-09-02
Summary
The charging arm of the battery that finally books what zod/v3 could only watch happen. Five findings, all inside the 4.2.0-4.3.0 window and all comfortably inside this subject's stated 2026-01 cutoff: one S1 (deriving from a refined object schema is asserted to load and silently drop the refinement, where 4.3.0 made it throw at module load) and four S2, three of which are flat denials that a capability exists - exact-optional keys, exclusive unions, and reading a JSON Schema document at runtime. The third of those is the costliest shape in the catalogue: the draw names z.fromJSONSchema by name while asserting it does not exist, and sends the reader to add Ajv instead. Every workaround the draw offered was executed against the published zod@4.5.4 and every one of them works, which is exactly why the charge rests on the denials and not on the code. The battery's own control deleted one probe outright - the slug task, where hand-rolling is simply correct - and complicated another, where a subject eighteen months below the floor stated the right parse output from wrong reasoning.
Subject
Claude Fable 5 claude-fable-5, Anthropic
Invoked as
Agent tool, model alias "fable", general-purpose subagent, instructed to use no tools; CHARGING TEST ARM of battery zod/v4
Cutoff the model states
2026-01
Newest zod release it could place
4.1.0 · 2025-08-23 (~5 month lag)
Oldest zod release it could not place
4.2.0 · 2025-12-15 (so this run brackets the subject’s boundary to 2025-08-23 – 2025-12-15)
In its own words
"The latest I know of is the 4.1.x patch line, with patches continuing through roughly October-November 2025. The most recent release whose contents I can actually describe is 4.1.0 (~August 2025) - the codecs release."
Library at test time
zod 4.5.4 (npm), verified 2026-09-02
Battery
zod/v4-a · 6 tasks, 4 direct questions · probe window 4.2.0 to 4.3.0
Tool uses during test
0 (a run with any tool use is void — we measure training knowledge, not retrieval)
F1 · States that deriving a sub-schema from a refined object schema loads without error and silently drops the refinement - the exact behaviour 4.3.0 replaced with a throw
S1breaks-build
· .pick() / .omit() on a schema with refinements
· now-throws
· changed in zod 4.3.0
(2025-12-31)
· chargeable
4.3.0 (2025-12-31) precedes this subject's stated cutoff of 2026-01.
What the model believes
"In Zod 4, refinements live inside the schema - .refine() on an object returns a ZodObject with a check attached - so the module loads without error, and PasswordOnly is a real ZodObject whose shape is { password: ZodString }. The cross-field refinement is not carried over to the picked schema: .pick/.omit/.extend rebuild from the shape and drop object-level refinements (which is fortunate here - a carried-over check would compare password against a now-missing confirmPassword and always fail). So PasswordOnly.parse({ password: \"x\" }) succeeds." The draw added: "The v4 docs call this dropping out explicitly."
What it wrote
const PasswordOnly = Signup.pick({ password: true }); // asserted to load and yield ZodObject<{password}>
What works on zod 4.5.4
const shape = { password: z.string(), confirmPassword: z.string() };
const Signup = z.object(shape).refine((d) => d.password === d.confirmPassword);
const PasswordOnly = z.object(shape).pick({ password: true }); // derive from the unrefined base
Impact
The belief is a description of pre-4.3 behaviour stated as current. On any zod at or above 4.3.0 the line throws at module evaluation, so a service that imports the module fails to start - not at the first parse, at load. The draw's own remedy is the right one and it wrote it for a different reason (error path placement), which means a reader who follows the prose rather than the final snippet ships the broken form.
Scope note
Re-executed against the published zod@4.5.4: the module in the task throws Error: .pick() cannot be used on object schemas containing refinements. The draw's premise that refinements moved inside the schema in v4 is correct; what is stale is the consequence it draws from it.
Using .pick() or .omit() on object schemas with refinements now throws an error. Previously, this would silently drop the refinements, leading to unexpected behavior.
F2 · Denies that the library has any way to make an object key omittable while rejecting an explicit undefined value
4.3.0 (2025-12-31) precedes this subject's stated cutoff of 2026-01.
What the model believes
"Plainly: Zod 4 has no first-class 'exact optional' wrapper. z.string().optional() means both 'key may be absent' and 'value undefined is accepted' - the two are not separable in the released API." Restated at (d)(ii): "an exact-optional wrapper ... does not exist in any released Zod version I know of."
A reader ships a superRefine that duplicates a built-in, and carries the belief that the library cannot express the contract their tsconfig is enforcing. The cost is not a broken parse - it is a wrong architectural conclusion about the library, restated confidently at (d)(ii) as a property of every released version.
Scope note
The charge is on the denial, not on the workaround. The hand-rolled superRefine was executed against zod@4.5.4 and it works: the omitted key passes and the explicit undefined is rejected. Under the JOURNAL/030 rule a working workaround is not a finding and a denial is, which is why this is charged and task 6 is not. The draw's claim that a trailing-? key constructor existed in the Zod 4 betas and was cut before 4.0 stable is recorded as an open question, not charged - JOURNAL/027 verified only that no such constructor exists in the published 4.x line.
4.2.0 (2025-12-15) precedes this subject's stated cutoff of 2026-01.
What the model believes
"Zod cannot do this. Zod 4 ships the reverse direction natively - z.toJSONSchema(schema) converts a Zod schema into JSON Schema - but there is no z.fromJSONSchema; Zod has no capability to consume an arbitrary JSON Schema document at runtime, in any release I know of." The draw then names the dependency it would add: "What I'd actually do: add Ajv for those payloads, even though that's a second validation library in the tree."
What it wrote
import Ajv from "ajv";
const validate = new Ajv().compile(externalJsonSchemaDoc);
This is the costliest shape of stale prior the Index has catalogued: the model does not write broken code, it correctly names the API that does not solve the problem, and then routes the reader to a dependency the library made unnecessary nine months earlier. The draw names the exact API by its exact name while asserting it does not exist.
Scope note
Executed against zod@4.5.4: z.fromJSONSchema on the object/required/minLength document in the fact accepts the valid payload and rejects the invalid one. The draw's reservation that upstream coverage of the full JSON Schema spec is partial is fair and upstream itself calls the API experimental (fact LF6); what is charged is the flat denial that the direction exists at all.
4.2.0 (2025-12-15) precedes this subject's stated cutoff of 2026-01.
What the model believes
"Note that z.union itself has no 'exactly one must match' semantics - it succeeds on the first matching branch. Exclusivity here comes entirely from strictObject making the both-keys case fail each branch." Restated at (d)(iii): "an exclusive union (exactly one branch must match, more than one matching is a failure): does not exist in Zod. ... You build XOR semantics yourself (strict branches or a refine)."
The workaround the draw substitutes is load-bearing on strictness rather than on exclusivity, and it says so. That is a real constraint the reader inherits: the moment the payload legitimately carries an extra field, the union stops being exclusive and the contract silently weakens to at-least-one. The draw anticipates this and proposes a superRefine for it, which is a second hand-rolled layer over a built-in.
Scope note
The charge is on the denial. The union-of-strictObjects workaround was executed against zod@4.5.4 and gives the four stated outcomes correctly for the four inputs in the task. Its stated limitation - exclusivity depends on strictness, not on the union - is the draw's own and is accurate.
4.3.0 (2025-12-31) precedes this subject's stated cutoff of 2026-01.
What the model believes
"C.parse({ a: \"x\", b: \"y\" }) throws a ZodError. ... z.intersection of strict objects is effectively unsatisfiable for any object with keys from both sides. The fix is to build one object instead."
What it wrote
C.parse({ a: "x", b: "y" }); // asserted: ZodError, unrecognized_keys from both sides
The reader is told a working composition is impossible and is sent to rewrite the schema as one flat object - which discards whatever made the two halves separate. The belief also has a defensive shape: it is stated as a known gotcha, which makes it less likely to be tested.
Scope note
Re-executed against zod@4.5.4: the schema exactly as written in the task parses to { a: "x", b: "y" }. Read alongside the control arm's context non-finding on the same probe - Claude Haiku 4.5, eighteen months below the floor, stated the correct output from incorrect reasoning, so this probe's OUTCOME is derivable. A derivable outcome makes a pass unreadable; it does not excuse a failure, and this draw stated the wrong outcome.
Zod 4 now only rejects keys that are unrecognized by both sides of the intersection.
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.
Kind
API
Note
correct
z.record()
THE INTERNAL CONTROL PASSED. (d)(i) placed the two-argument requirement on the dictionary constructor at "first stable in zod@3.25.0's zod/v4 subpath (May 2025), i.e. in zod@4.0.0 as a standalone major (July 2025). Not a patch; it was a headline breaking change of the major." That is correct on both the release and the shape of the release (fact LF5), and it is the second library at which JOURNAL/030's rule holds: where a subject holds the capability, it dates a MAJOR correctly. The rest of question (d) is therefore readable for this arm. (It is the control, and it passed.)
miss
z.slugify()
Task 6, the slug probe: hand-rolled a lowercase/normalize/replace chain where z.slugify() has existed since 4.3.0 (fact LF17). The code works - executed against zod@4.5.4, the built-in returns "hello-world" for "Hello, World!" and so does the hand-rolled chain. The draw never denied a built-in existed; it simply did not reach for one. (JOURNAL/030's rule: the capability probe charges denial, not workarounds. There is no severity level for correct code that a first-class API supersedes, and inventing one is what the scale resists. This probe was written expecting to land here.)
context
—
The two blind Fable 5 draws agree on all six probes, including all five charged surfaces, and on the boundary (4.1.0 / 4.2.0). Set against zod/v3 and better-auth/v2, where the -b twin held the better answer three batteries running, this is the first duplicated test arm in the Index that produced no disagreement at all to report. (It is a property of the instrument, not of the library.)
context
—
Third battery, same boundary. This subject placed its zod attribution boundary at 4.1.0 / 4.2.0 in zod/v2 (2026-08-29), in zod/v3-d (2026-09-02) and again here, across three batteries that share no wording. That is JOURNAL/030's 'a different battery is not a different boundary' result, now shown at a second library. (The boundary belongs to the run that first measured it; a later battery reproducing it is context. Never re-charged.)
Open questions from this run
Did a trailing-? key-optional object constructor (z.interface()) ever ship in a published Zod 4 beta, as both Fable 5 draws assert - "the Zod 4 betas had this, via z.interface() and its 'nickname?' key syntax, and it was removed before 4.0 stable"? — open: JOURNAL/027 verified that no such constructor exists in the published 4.x line, and that the betas reachable on the CDN from 3.25.20 to 3.25.29 do not carry it. The versions that would settle whether it existed earlier are not published. Under the zod/v3 scope limit this claim is therefore not charged against either Fable draw. Both draws made it independently and in near-identical terms, which raises rather than lowers the value of settling it.