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

  1. Pass { unrepresentable: 'any' } to toJSONSchema(); the dynamic catch is skipped (no `default` emitted) instead of throwing.
  2. 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.
  3. 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

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


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)