colinhacks/zod · error · Error

Invalid input to safeExtend: expected a plain object

Error message

Invalid input to safeExtend: expected a plain object

What it means

Thrown by safeExtend() when the shape argument fails the same isPlainObject() guard used by extend(). `.safeExtend()` is the refinement-safe variant of `.extend()` (it skips the check-overlap guard) but still requires a plain object literal as its shape argument. Class instances, arrays, Maps, null, and primitives are rejected.

Solutions

  1. Pass a plain object literal of key→schema pairs.
  2. Coerce wrappers (Map, class, Record subclass) into a plain object via spread or Object.fromEntries before calling `.safeExtend()`.
  3. Confirm the argument is not null, an array, or a primitive.

Example fix

// before
const pairs: [string, z.ZodType][] = [['id', z.number()]];
Base.safeExtend(pairs); // throws

// after
Base.safeExtend(Object.fromEntries(pairs));
Defensive patterns

Strategy: type-guard

Validate before calling

function asSafeShape(shape: unknown): Record<string, z.ZodType> {
  if (!isPlainShape(shape)) {
    throw new TypeError('safeExtend() expects a plain object of key -> ZodType');
  }
  return shape as Record<string, z.ZodType>;
}
// base.safeExtend(asSafeShape(maybeShape));

Type guard

function isPlainShape(o: unknown): o is Record<string, z.ZodType> {
  if (o === null || typeof o !== 'object' || Array.isArray(o)) return false;
  const proto = Object.getPrototypeOf(o);
  return proto === null || proto === Object.prototype;
}

Prevention

When it happens

Trigger: Calling `schema.safeExtend(classInstance)`, `schema.safeExtend([...])`, `schema.safeExtend(null)`, or any non-plain-object value. Same input contract as `.extend()`.

Common situations: Same as the extend() case: passing a typed Record, a schema object, an array of pairs, or a wrapper class instead of a literal shape map; migrating from `.extend()` to `.safeExtend()` without fixing an already-wrong argument type.

Related errors


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

Appendix: source

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

      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");
  }
  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 };

View on GitHub (pinned to 2d90846af9)