fix(toJSONSchema): preserve default value through pipe/transform in input mode#6064
fix(toJSONSchema): preserve default value through pipe/transform in input mode#6064VihAMBR wants to merge 1 commit into
Conversation
…nput mode
z.string().transform(s => s).default("hello") with io: "input" was silently
dropping the default keyword. The process() function unconditionally deleted
result.schema.default whenever isTransforming() returned true, which affected
ZodDefault schemas whose inner type contains any transform — even identity
transforms. Since ZodDefault explicitly declares the input-side default value,
the deletion should be skipped for it.
Fixes colinhacks#6049
There was a problem hiding this comment.
Reviewed changes — fixes z.toJSONSchema() dropping default when a ZodDefault wraps a pipeline containing a transform in io: "input" mode. The isTransforming check recurses into ZodDefault's inner type, so any transform inside caused process() to unconditionally wipe the default value that defaultProcessor just set. The fix guards the delete with def.type !== "default".
- Guard
delete result.schema.defaultwithdef.type !== "default"inprocess()—ZodDefaultalways declares an explicit input-side default, so removing it here is wrong regardless of whether the inner type transforms.ZodPrefaultalready had equivalent protection at L211 viaresult.schema.default ??= result.schema._prefault. - Add regression test for #6049 — covers both the reported
z.string().transform(s => s).default("hello")case and thez.string().transform(...).pipe(z.string()).default("hello")variant. - Update existing inline snapshot for
a.default(1234)with a type-changing transform to reflect the now-correctly-preserveddefault: 1234.
✅ No new issues found.
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
wahajahmed010
left a comment
There was a problem hiding this comment.
A clean, well-documented fix. The root-cause analysis is excellent — isTransforming runs after every processor and the unconditional delete result.schema.default was colliding with defaultProcessor.
Logic: Correct. Checking def.type !== "default" is the right discriminant — only ZodDefault intentionally sets result.schema.default on the input side. One subtle edge case: if a future processor also sets default on input for a non-ZodDefault schema, this guard would not cover it. A snapshot approach ("was it set before?") would be more robust, but that is future-proofing, not a current bug.
Tests: Snapshot update and new #6049 repro test look correct. The pipe-through-transform test covering both sides is thorough.
Edge case: The static def.type check assumes ZodDefault def.type is always "default". From reading the Zod source — confirmed, it is. Safe.
Nice work — minimal, correct, well-explained.

Problem
z.string().transform(s => s).default("hello")passed toz.toJSONSchema({ io: "input" })silently drops thedefaultkeyword:Fixes #6049.
Root cause
process()into-json-schema.tsrunsisTransforming(schema)after every processor and unconditionally deletesresult.schema.defaultwhen the check returnstrue. For aZodDefaultwhose inner type is aZodPipecontaining a transform,isTransformingreturnstrue— so thedefaultvalue thatdefaultProcessorjust set on the schema is immediately wiped.The deletion was intended for cases where
defaultis inherited from a pipe (output-side default), not forZodDefault, which always declares an explicit input-side default.Fix
Skip the
delete result.schema.defaultstep whendef.type === "default". TheZodDefaultprocessor always setsdefaultintentionally; removing it here is wrong regardless of whether the inner type transforms.Test changes
a.default(1234)(type-changing transform): with this fix,default: 1234now appears in the input schema. Thedefaultkeyword in JSON Schema is advisory and does not need to validate against the schema type, so this is spec-compliant and more informative for tooling.toJSONSchemadropsdefaultwhen the defaulted schema contains a transform/pipe #6049.