colinhacks/zod · error · Error

Cannot overwrite keys on object schemas containing…

Error message

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

What it means

Thrown by extend() only when the source object carries refinements AND the new shape would overwrite an existing key. Overwriting a field while object-level checks are present would silently change what the refinements validate, so Zod refuses and points to `.safeExtend()`, which intentionally bypasses the check-overlap guard. Purely additive keys on a refined object do NOT trigger this.

Solutions

  1. Use `.safeExtend({ ...overwrittenKey })` if you intentionally accept that object-level refinements now validate against the new field type.
  2. Drop or relocate the refinement so `.extend()` can overwrite freely.
  3. Build a new object schema from scratch combining the wanted fields and re-apply the refinement explicitly.

Example fix

// before
const Base = z.object({ id: z.string() }).refine(v => v.id.length > 0);
const Extended = Base.extend({ id: z.number() }); // throws

// after — explicit, refinement-aware overwrite
const Extended = Base.safeExtend({ id: z.number() });
Defensive patterns

Strategy: validation

Validate before calling

function canExtendOverwrite(schema: z.ZodObject, shape: Record<string, unknown>): boolean {
  const checks = (schema as any)._zod?.def?.checks;
  if (!checks || checks.length === 0) return true;
  const existing = (schema as any)._zod.def.shape;
  for (const k of Object.keys(shape)) {
    if (Object.prototype.hasOwnProperty.call(existing, k)) return false;
  }
  return true;
}
// if (!canExtendOverwrite(Base, { id: z.number() })) Base = Base.safeExtend({ id: z.number() });

Type guard

function extendWouldOverwrite(schema: z.ZodObject, shape: Record<string, unknown>): boolean {
  const existing = (schema as any)._zod.def.shape;
  return Object.keys(shape).some((k) => Object.prototype.hasOwnProperty.call(existing, k));
}

Prevention

When it happens

Trigger: Calling `UserWithRefine.extend({ email: z.number() })` where `email` already exists and `UserWithRefine` has `.refine()`/`.check()`. The guard iterates the new shape and throws on the first key found in the existing shape via `Object.getOwnPropertyDescriptor`.

Common situations: Overriding an inherited/base field on a refined object (e.g. widening `id` from string to string|number); composing schemas where a base carries invariants; refactoring a field type on a schema that gained a refinement upstream.

Related errors


AI-assisted analysis of colinhacks/zod@2d90846af9 (2026-08-11). Data as JSON: /api/errors/770e059bcd82d0fa. Report an issue: GitHub.

Appendix: 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 2d90846af9)