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 shape argument fails isPlainObject(). Zod requires the shape to be a plain object literal (prototype of Object.prototype or no modified constructor/prototype) so it can safely spread keys as field schemas. Class instances, arrays, Maps, null, and primitives are rejected before any keys are read.

Solutions

  1. Pass a plain object literal: `{ newField: z.string() }`.
  2. If your shape lives in a class/Map/wrapper, spread it into a fresh `{}` first: `schema.extend({ ...mapOrInstance })` after extracting the schema entries.
  3. Ensure the value is not an array, null, or a primitive — extend takes a key→schema map only.

Example fix

// before
const extra = new Map([['id', z.string()]]);
Base.extend(extra); // throws

// after
const extra = { id: z.string() };
Base.extend(extra);
Defensive patterns

Strategy: type-guard

Validate before calling

function asShape(shape: unknown): Record<string, z.ZodType> {
  if (!isPlainShape(shape)) {
    throw new TypeError('extend() expects a plain object of key -> ZodType');
  }
  return shape as Record<string, z.ZodType>;
}
// base.extend(asShape(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.extend(SomeClassInstance)`, `schema.extend([...])`, `schema.extend(null)`, or passing a schema object, a Record subclass, or a Proxy whose constructor/prototype has been swapped. Also triggered by passing a `z.object(...)` result instead of its inner shape.

Common situations: Passing a class instance (e.g. a typed Record from a serializer) instead of a literal; accidentally feeding an array of [key, schema] pairs; handing in a schema instead of a shape map; using a custom object wrapper that mutates the prototype.

Related errors


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

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