Summary
When a schema is wrapped in .default() and the inner schema contains a .transform() (i.e. a ZodPipe), z.toJSONSchema(..., { io: "input" }) omits the default keyword from the output. Removing the transform makes default appear correctly, so the transform's mere presence suppresses the default value.
Zod version
zod@4.4.3
Minimal reproduction
import { z } from "zod";
const withoutTransform = z.string().default("hello");
const withTransform = z.string().transform((s) => s).default("hello");
console.log(z.toJSONSchema(withoutTransform, { io: "input" }));
// { "$schema": "...", "default": "hello", "type": "string" } ✅
console.log(z.toJSONSchema(withTransform, { io: "input" }));
// { "$schema": "...", "type": "string" } ❌ default dropped
Expected
Both should emit default: "hello". The transform changes neither the input type (still string) nor the input-side default value, so the default keyword should be preserved:
{ "type": "string", "default": "hello" }
Actual
The default keyword is missing from the output whenever the defaulted schema contains a transform/pipe.
Notes
- Reproduces in
io: "input" mode (shown above). It also drops the default in io: "output" mode, but that path additionally requires unrepresentable: "any" (otherwise it throws on the transform).
- It also reproduces when the pipe is nested deeper — e.g. an object property with a transform inside a
z.union([...]).default(...):
z.toJSONSchema(
z.union([z.boolean(), z.object({ a: z.string().transform((x) => x) })]).default(false),
{ io: "input" },
);
// `default: false` is dropped
- Appears to be in the interaction between the default processor and the pipe processor (
defaultProcessor → pipeProcessor in core/json-schema-processors).
Summary
When a schema is wrapped in
.default()and the inner schema contains a.transform()(i.e. aZodPipe),z.toJSONSchema(..., { io: "input" })omits thedefaultkeyword from the output. Removing the transform makesdefaultappear correctly, so the transform's mere presence suppresses the default value.Zod version
zod@4.4.3Minimal reproduction
Expected
Both should emit
default: "hello". The transform changes neither the input type (stillstring) nor the input-side default value, so thedefaultkeyword should be preserved:{ "type": "string", "default": "hello" }Actual
The
defaultkeyword is missing from the output whenever the defaulted schema contains a transform/pipe.Notes
io: "input"mode (shown above). It also drops the default inio: "output"mode, but that path additionally requiresunrepresentable: "any"(otherwise it throws on the transform).z.union([...]).default(...):defaultProcessor→pipeProcessorincore/json-schema-processors).