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 argument is not a plain object (guard at util.ts:688, same `isPlainObject` check as `.extend()`). `.safeExtend()` is the refinement-tolerant variant of `.extend()`, but it still requires a literal shape map of `ZodType` values.

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 912f0f51b0)

Solutions

  1. Pass a plain object literal of field schemas: `schema.safeExtend({ field: z.string() })`.
  2. If building dynamically, use `Object.fromEntries()` to materialize a plain object first.
  3. Add a TypeScript annotation `const shape: z.core.$ZodShape = {...}` to surface shape errors at compile time.

Example fix

// before
const Extended = User.safeExtend(SomeSchema);

// after
const Extended = User.safeExtend({ field: SomeSchema });
Defensive patterns

Strategy: type-guard

Validate before calling

function isPlainShape(v: unknown): v is Record<string, z.core.$ZodType> {
  return typeof v === 'object' && v !== null &&
    [Object.prototype, null].includes(Object.getPrototypeOf(v));
}
if (!isPlainShape(arg)) throw new Error('safeExtend needs a plain object');
const Out = schema.safeExtend(arg);

Type guard

function isZodShape(v: unknown): v is z.core.$ZodShape {
  return typeof v === 'object' && v !== null &&
    [Object.prototype, null].includes(Object.getPrototypeOf(v)) &&
    Object.values(v).every(x => x && typeof x === 'object' && '_zod' in x);
}

Prevention

When it happens

Trigger: Calling `schema.safeExtend(null)`, `schema.safeExtend(array)`, or passing a non-plain-object value where a shape record is expected.

Common situations: Migrating a `.extend()` call to `.safeExtend()` to bypass refinement guards but keeping a malformed argument; passing a single schema instance instead of `{ key: schema }`; deserializing shapes from config without reconstruction.

Related errors


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