colinhacks/zod · error · Error

Invalid input to extend: expected a plain object

Error message

Invalid input to extend: expected a plain object

What it means

Thrown by `.extend()` when the argument is not a plain object — checked via `isPlainObject(shape)` at util.ts:660. `.extend()` needs a literal shape map of `ZodType` values; passing arrays, class instances, `null`, `undefined`, or anything with a non-standard prototype is rejected upfront. This prevents silent spread-merge of unexpected inputs into the schema's shape.

Source

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

        if (!(key in currDef.shape)) {
          throw new Error(`Unrecognized key: "${key}"`);
        }
        if (!(mask as any)[key]) continue;

        delete newShape[key];
      }
      assignProp(this, "shape", newShape); // self-caching
      return newShape;
    },
    checks: [],
  });

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

View on GitHub (pinned to 912f0f51b0)

Solutions

  1. Wrap the input as an object literal: `schema.extend({ field: z.string() })` rather than `schema.extend(z.string())`.
  2. If building dynamically from entries, convert with `Object.fromEntries(entries)` before passing.
  3. If you intended to merge two object schemas, use `.merge(other)` instead of `.extend(other)`.

Example fix

// before
const Extended = User.extend(z.string());

// after
const Extended = User.extend({ nickname: z.string() });
Defensive patterns

Strategy: type-guard

Validate before calling

function isPlainShape(v: unknown): v is Record<string, z.core.$ZodType> {
  return Object.getPrototypeOf(v) === Object.prototype || Object.getPrototypeOf(v) === null;
}
if (!isPlainShape(arg)) throw new Error('shape must be a plain object');
const Out = schema.extend(arg);

Type guard

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

Prevention

When it happens

Trigger: Calling `schema.extend(someArray)` or `schema.extend(someZodType)` or `schema.extend(JSON.parse(json))` where the result is not a plain object literal of field schemas.

Common situations: Dynamically building a shape from `Object.values()` (array) and passing it directly; passing a single `z.string()` instead of `{ field: z.string() }`; passing a `Map` or other iterable; deserializing shapes from JSON without reconstructing Zod types.

Related errors


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