colinhacks/zod · error · Error
Dynamic catch values are not supported in JSON Schema
Error message
Dynamic catch values are not supported in JSON Schema
What it means
Thrown by the catchProcessor when a ZodCatch schema uses a *function* catch value (z.catch(() => computeDefault())) and that function throws when invoked, while unrepresentable is 'throw' (default). The converter tries to call the catch function to obtain a concrete default for JSON Schema's `default` keyword; a throwing or side-effecting catch function cannot be serialised and is rejected.
Solutions
- Pass { unrepresentable: 'any' } to toJSONSchema(); the dynamic catch is skipped (no `default` emitted) instead of throwing.
- Provide a static catch value (a literal or pure expression) for the schema used in JSON Schema export, and keep the dynamic catch only in the runtime schema.
- Make the catch function total — never throw, always return a JSON-serialisable constant — so the converter can sample it safely.
Example fix
// before (throws if readEnv throws)
const Schema = z.string().catch(() => readEnv('DEFAULT')!);
z.toJSONSchema(Schema);
// after (static value for export)
const ExportSchema = z.string().catch('fallback');
z.toJSONSchema(ExportSchema);
// { type: 'string', default: 'fallback' }
// or skip it
z.toJSONSchema(Schema, { unrepresentable: 'any' }); Defensive patterns
Strategy: try-catch
Validate before calling
// Detect dynamic catch functions before exporting.
function isDynamicCatch(schema) {
return schema._zod.def.type === 'catch' && typeof schema._zod.def.catchValue === 'function';
}
const opts = schemaContains(schema, isDynamicCatch)
? { unrepresentable: 'any' }
: {};
const json = z.toJSONSchema(schema, opts); Type guard
function hasDynamicCatch(schema) {
if (schema._zod.def.type !== 'catch') return false;
// catchValue is always a function in the def; test whether it throws when sampled
try {
schema._zod.def.catchValue(undefined);
return false;
} catch {
return true;
}
} Try / catch
try {
return z.toJSONSchema(schema);
} catch (e) {
if (e.message === 'Dynamic catch values are not supported in JSON Schema') {
// Omit the default by allowing the any fallback
return z.toJSONSchema(schema, { unrepresentable: 'any' });
}
throw e;
} Prevention
- Use static catch values (literals) in schemas you export; keep dynamic catch functions runtime-only.
- If a catch must read runtime state, make it total (never throw) and return a JSON-serialisable constant.
- Pass { unrepresentable: 'any' } when exporting schemas that may include dynamic catches.
When it happens
Trigger: Defining z.catch(() => { throw new Error('no default') }) or z.catch(() => someUndefinedVar.foo) and calling toJSONSchema() on the schema. A catch function that depends on runtime state unavailable during schema export (env, DB, request context).
Common situations: Catch values that read config at call time, randomised fallbacks, or catches that legitimately only make sense during parsing — all of which break static JSON Schema generation.
Related errors
- BigInt cannot be represented in JSON Schema
- BigInt literals cannot be represented in JSON Schema
- Custom types cannot be represented in JSON Schema
- Date cannot be represented in JSON Schema
- Function types cannot be represented in JSON Schema
AI-assisted analysis of colinhacks/zod@2d90846af9 (2026-08-11).
Data as JSON: /api/errors/7a266e4ca8109c66.
Report an issue: GitHub.
Appendix: source
Thrown at packages/zod/src/v4/core/json-schema-processors.ts:528
export const prefaultProcessor: Processor<schemas.$ZodPrefault> = (schema, ctx, json, params) => {
const def = schema._zod.def as schemas.$ZodPrefaultDef;
process(def.innerType, ctx as any, params);
const seen = ctx.seen.get(schema)!;
seen.ref = def.innerType;
if (ctx.io === "input") json._prefault = JSON.parse(JSON.stringify(def.defaultValue));
};
export const catchProcessor: Processor<schemas.$ZodCatch> = (schema, ctx, json, params) => {
const def = schema._zod.def as schemas.$ZodCatchDef;
process(def.innerType, ctx as any, params);
const seen = ctx.seen.get(schema)!;
seen.ref = def.innerType;
let catchValue: any;
try {
catchValue = def.catchValue(undefined as any);
} catch {
if (ctx.unrepresentable === "throw") {
throw new Error("Dynamic catch values are not supported in JSON Schema");
}
return;
}
json.default = catchValue;
};
export const pipeProcessor: Processor<schemas.$ZodPipe> = (schema, ctx, _json, params) => {
const def = schema._zod.def as schemas.$ZodPipeDef;
const inIsTransform = def.in._zod.traits.has("$ZodTransform");
const innerType = ctx.io === "input" ? (inIsTransform ? def.out : def.in) : def.out;
process(innerType, ctx as any, params);
const seen = ctx.seen.get(schema)!;
seen.ref = innerType;
};
export const readonlyProcessor: Processor<schemas.$ZodReadonly> = (schema, ctx, json, params) => {
const def = schema._zod.def as schemas.$ZodReadonlyDef;
process(def.innerType, ctx as any, params);View on GitHub (pinned to 2d90846af9)