Zod 4.6.7 omits every .refine() from the JSON Schema it hands to structured-output models
Zod 4.6.7's JSON Schema converter has 13 check keys and none is .refine(), according to a dev.to reading of its source. The post says the OpenAI Node SDK and Vercel AI SDK both use it, so custom rules run only after parsing.
The Engineer · Build desk
The OpenAI Node SDK and Vercel AI SDK pass Zod 4 schemas to models via z.toJSONSchema(), Vercel in io: "input" mode. .refine(), object .refine() and .superRefine() become custom checks the converter skips, with no warning. A string transform is shown as "string"; Number() can yield NaN.
- Who's exposed exposure OpenAI Node SDK Calls z.toJSONSchema() on Zod 4 schemas; that JSON is what constrains the model, claim 4
- Who has to decide decision Vercel AI SDK Uses the io: "input" mode when converting schemas, claim 13
- Who's exposed exposure Structured-output helpers Passing a Zod 4 schema drops every .refine() before the model sees it, with no warning, claim 6
- What it limits constraint .refine() rules Stored as a check named custom, which is not a contributors key, so the rule is skipped, claim 3
- What it limits constraint Object .refine(), .superRefine() Both also land as custom checks, claim 15
- Who's exposed exposure String transforms Input mode tells the model only "string"; Number() can return NaN with no validation error, claim 12
| Who | How | Kind | Claim |
|---|---|---|---|
| OpenAI Node SDK | Calls z.toJSONSchema() on Zod 4 schemas; that JSON is what constrains the model | exposure | 4 |
| Vercel AI SDK | Uses the io: "input" mode when converting schemas | decision | 13 |
| Structured-output helpers | Passing a Zod 4 schema drops every .refine() before the model sees it, with no warning | exposure | 6 |
| .refine() rules | Stored as a check named custom, which is not a contributors key, so the rule is skipped | constraint | 3 |
| Object .refine(), .superRefine() | Both also land as custom checks | constraint | 15 |
| String transforms | Input mode tells the model only "string"; Number() can return NaN with no validation error | exposure | 12 |
What happened
- Zod stores every .refine() as a check named custom in api.ts, and that name is not among the keys the converter's contributors map handles.
- In the post's refund schema, regex, positive, enum and ISO date rules have JSON Schema keywords, while the note refine, the object-level date refine and the sku transform do not.
- In input mode Zod's docs say additionalProperties is not set on objects, and the Vercel AI SDK adds it back with its own addAdditionalPropertiesToJsonSchema pass.
- For z.string().transform(Number), input mode tells the model only "string", and Number() can return NaN with no validation error, according to the post.
Why it matters
- Who's exposed exposure A Zod 4 schema with .refine(), .superRefine() or an object-level check gives the model a weaker contract than the parser enforces.
- What it costs cost A broken custom rule surfaces only after the model has answered, so the first signal is a failed parse on a response already generated and paid for.
- Who has to decide decision Each rule now needs a placement: a keyword-expressible check, the prompt text, or the parser alone. We'd restate any rule the output depends on in the prompt, because the schema will not carry it.
The silence comes from one line. In v4.6.7, the release of 12 October 2026, the converter loops over a schema's checks and runs `contributors[ch._zod.def.check]?.(agg, ch._zod.def)` for each [1]. The map holds 13 keys, among them greater_than, less_than, multiple_of, min_length and string_format [2]. Written as an optional call, a lookup that misses does nothing and raises nothing [1]. That matches the post's description of a drop with no warning [6].
Take the refund schema in the post. Its object-level refine requires refundDate to be on or after orderDate [14]. That refine is stored as a custom check too, so the schema the model receives describes two ISO date strings and says nothing about their order [17].
The evidence has limits. The code reading comes from a single dev.to post, and its JSON output is a reconstruction. The author wrote, "I haven't run this exact schema myself." [16] Each property was built from the converter's test snapshots and processor source, with the long date regex and the $schema line trimmed [8].
The strongest evidence in the post is a snapshot in to-json-schema.test.ts. The test puts two refined string schemas in a union, one requiring a length above 2 and the other a length below 9, and the result collapses to a bare "type": "string" [9]. The post reasons that the two branches can dedupe only if neither refinement reached the output [10].
For the finding to hold in your stack, your Zod version's map must still lack a custom entry, and your SDK version must route Zod 4 schemas through z.toJSONSchema() [4]. The post reads v4.6.7 only [1]. It gives a one-line check: console.log(JSON.stringify(z.toJSONSchema(Refund, { io: "input" }), null, 2)) [8]. That call uses the input mode, which the post says the Vercel AI SDK uses [13].
What to watch
- Whether Zod adds a custom entry or a warning for skipped checks in a release after v4.6.7.
- Whether the OpenAI Node SDK or Vercel AI SDK reports dropped checks when it post-processes the schema.
- A schema printed from a real install, since the post's JSON output is a reconstruction.
Clarity's read
What the record supports and how the coverage leans. The claims behind it follow.
Reality
- Evidence62
- Adoption
- Insufficient
- Hype gap+8
- Incentives25
- Confidence58
Claim ledger
Ranked by verification strength, evidence, and original report placement.
- [1]
In Zod v4.6.7, released 12 October 2026, json-schema-processors.ts loops over each check and runs contributors[ch._zod.def.check]?.(agg, ch._zod.def) to write validation rules into the schema.
- [2]
The contributors map has 13 keys, including greater_than, less_than, multiple_of, min_length and string_format.
- [3]
Every .refine() is stored as a check named custom (api.ts), and custom is not one of the contributors keys, so the rule is skipped.
- [4]
The OpenAI Node SDK and the Vercel AI SDK both call z.toJSONSchema() on Zod 4 schemas, and that JSON is what constrains the model.
- [5]
The refinement still runs later, when the response is parsed; the model never heard about it.
- [6]
Passing a Zod 4 schema to a structured-output helper drops every .refine() before the model sees it, with no warning.
- [7]
In the refund schema, regex, positive, z.enum and the ISO date format have JSON Schema keywords; a .refine() on note, a cross-field .refine() on the whole object and a .transform() on sku do not.
- [8]
Each property in the post's printed JSON follows the converter's test snapshots in to-json-schema.test.ts and its processor source; the long ISO-date regex and the $schema line were trimmed; the post suggests printing it with console.log(JSON.stringify(z.toJSONSchema(Refund, { io: "input" }), null, 2)).
- [9]
The test file unions z.string().refine((s) => s.length > 2) with z.string().refine((s) => s.length < 9), and the snapshot collapses both branches to a bare "type": "string".
- [10]
The two branches can only dedupe if neither refinement made it into the output.
- [11]
Per the Zod JSON Schema docs, in input mode additionalProperties is not set on objects; the AI SDK adds it back with its own addAdditionalPropertiesToJsonSchema pass; in the default output mode Zod writes additionalProperties: false itself.
- [12]
In input mode a string transform is read from its string side, so for z.string().transform(Number) the model is told only "string", nothing stops it writing "twelve" or "12 EUR", and Number() hands the code NaN without a validation error.
- [13]
The io: "input" mode is the one the Vercel AI SDK uses.
- [14]
The refund schema has an object-level .refine() requiring r.refundDate >= r.orderDate, with the message "refundDate is before orderDate".
- [15]
.superRefine() and object .refine() also land as custom checks.
- [16]
I haven't run this exact schema myself.
- [17]
The date-order rule in the refund schema is stored as a custom check, custom is absent from the contributors map, so the schema the model receives does not state that refundDate must be on or after orderDate.
- [18]
Because dropped refinements run only at parse time, a violation is first detected after the model has already produced its response.
Get this ledger inside ChatGPT. Ask about any of our stories and the answer brings its figure, each claim’s verdict and the sources. Free, no account.
Sources
1 independent publisher whose own reporting we read for this story.
- dev.toYour Zod .refine() Never Reaches the Model: What Survives the Trip to JSON Schema
1 article · October 15, 2026
Topics and entities
Follow any of these and your For You feed starts watching them — no settings page required.
Entities
- ZodFollow
- Vercel AI SDKFollow
- OpenAI Node SDKFollow
- JSON SchemaFollow