colinhacks/zod · error · Error

Cannot overwrite keys on object schemas containing refinemen

Error message

Cannot overwrite keys on object schemas containing refinements. Use `.safeExtend()` instead.

What it means

Thrown by `.extend()` when the schema has refinement checks AND the new shape tries to overwrite a key that already exists (guard at util.ts:666-673). Overwriting a field that a refinement depends on would silently break cross-field validation, so the library refuses and redirects to `.safeExtend()`, which intentionally bypasses the check.

Source

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

  });

  return clone(schema, def);
}

export function extend(schema: schemas.$ZodObject, shape: schemas.$ZodShape): any {
  if (!isPlainObject(shape)) {
    throw new Error("Invalid input to extend: expected a plain object");
  }

  const checks = schema._zod.def.checks;
  const hasChecks = checks && checks.length > 0;
  if (hasChecks) {
    // Only throw if new shape overlaps with existing shape
    // Use getOwnPropertyDescriptor to check key existence without accessing values
    const existingShape = schema._zod.def.shape;
    for (const key in shape) {
      if (Object.getOwnPropertyDescriptor(existingShape, key) !== undefined) {
        throw new Error("Cannot overwrite keys on object schemas containing refinements. Use `.safeExtend()` instead.");
      }
    }
  }

  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 safeExtend(schema: schemas.$ZodObject, shape: schemas.$ZodShape): any {
  if (!isPlainObject(shape)) {
    throw new Error("Invalid input to safeExtend: expected a plain object");
  }

View on GitHub (pinned to 912f0f51b0)

Solutions

  1. Use `.safeExtend({ key: newSchema })` if you intentionally accept the refinement-risk tradeoff.
  2. Move the refinement off the base schema and reattach it after `.extend()`.
  3. Define the base object without refinements, extend it, then layer `.refine()` on each variant.

Example fix

// before
const Base = z.object({ id: z.string() }).refine(d => d.id.length > 0);
const Wide = Base.extend({ id: z.string().uuid() }); // throws

// after
const Wide = Base.safeExtend({ id: z.string().uuid() });
Defensive patterns

Strategy: validation

Validate before calling

function hasOverlaps(s: z.core.$ZodObject, shape: Record<string, unknown>): boolean {
  const existing = s._zod.def.shape;
  return Object.keys(shape).some(k => Object.getOwnPropertyDescriptor(existing, k) !== undefined);
}
if (hasRefinements(Schema) && hasOverlaps(Schema, newShape)) {
  // use safeExtend intentionally
}

Type guard

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

Try / catch

try { Out = Schema.extend(newShape); }
catch (e) {
  if (e instanceof Error && /safeExtend/.test(e.message)) Out = Schema.safeExtend(newShape);
  else throw e;
}

Prevention

When it happens

Trigger: Calling `Schema.extend({ existingKey: z.string() })` where `Schema` was built with `.refine()/.superRefine()` touching `existingKey`.

Common situations: Overriding a field type on a validated schema (e.g. widening `id` from `z.string()` to `z.string().uuid()`); reusing a base schema with refinements and customizing fields per endpoint; inheriting a shared validated schema.

Related errors


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