colinhacks/zod · error · Error

.merge() cannot be used on object schemas containing refinem

Error message

.merge() cannot be used on object schemas containing refinements. Use .safeExtend() instead.

What it means

Thrown by `.merge()` when the first argument (`a`) has refinement checks (`a._zod.def.checks?.length` at util.ts:702). `.merge()` would silently drop or relocate those refinements during the shape combine, so it refuses and points to `.safeExtend()`, which is the intended refinement-safe composition tool.

Source

Thrown at packages/zod/src/v4/core/util.ts:703

}

export function safeExtend(schema: schemas.$ZodObject, shape: schemas.$ZodShape): any {
  if (!isPlainObject(shape)) {
    throw new Error("Invalid input to safeExtend: expected a plain object");
  }
  const def = mergeDefs(schema._zod.def, {
    get shape() {
      const _shape = { ...schema._zod.def.shape, ...shape };
      assignProp(this, "shape", _shape); // self-caching
      return _shape;
    },
  });
  return clone(schema, def) as any;
}

export function merge(a: schemas.$ZodObject, b: schemas.$ZodObject): any {
  if (a._zod.def.checks?.length) {
    throw new Error(".merge() cannot be used on object schemas containing refinements. Use .safeExtend() instead.");
  }
  const def = mergeDefs(a._zod.def, {
    get shape() {
      const _shape = { ...a._zod.def.shape, ...b._zod.def.shape };
      assignProp(this, "shape", _shape); // self-caching
      return _shape;
    },
    get catchall() {
      return b._zod.def.catchall;
    },
    checks: b._zod.def.checks ?? [],
  });

  return clone(a, def) as any;
}

export function partial(
  Class: SchemaClass<schemas.$ZodOptional> | null,

View on GitHub (pinned to 912f0f51b0)

Solutions

  1. Use `a.safeExtend(b.shape)` (or spread `b._zod.def.shape` into a plain object) as the suggested workaround.
  2. Pull refinements off `a` and reattach them after `.merge()`.
  3. Define `a` as the bare object and attach refinements only on the final composed schema.

Example fix

// before
const Merged = Validated.merge(Extra);

// after
const Merged = Validated.safeExtend(Extra._zod.def.shape);
Defensive patterns

Strategy: validation

Validate before calling

function hasRefinements(s: z.core.$ZodObject): boolean {
  return !!s._zod.def.checks?.length;
}
if (hasRefinements(a)) {
  // merge manually via safeExtend
  const merged = a.safeExtend(b._zod.def.shape);
}

Type guard

function isRefinementFree(s: z.core.$ZodObject): boolean {
  return !s._zod.def.checks?.length;
}

Try / catch

try { const Out = a.merge(b); }
catch (e) {
  if (e instanceof Error && /merge.*safeExtend/.test(e.message)) {
    const Out = a.safeExtend(b._zod.def.shape);
  } else throw e;
}

Prevention

When it happens

Trigger: Calling `ValidatedSchema.merge(OtherSchema)` where `ValidatedSchema` is `z.object({...}).refine(...)` or has any check in `def.checks`.

Common situations: Composing a base object with cross-field validation against a second object schema; sharing validated base schemas across endpoints and merging in per-endpoint fields; migrating from v3 where `.merge()` was more permissive.

Related errors


AI-assisted analysis of colinhacks/zod@912f0f51b0 (2026-08-03). Data as JSON: /data/errors/bf96072a776a8fe6.json. Report an issue: GitHub.