ZodError: [ { "code": "invalid_type", "expected": "string", "received": "undefined", "path": [ "email" ], "message": "Required" } ]

Node.js · updated 2026-11-03

Thrown by schema.parse() when the data does not match the schema. The error is an array of issues, each with path (which field), code (what kind of failure), expected and received. A 500 here usually means untrusted input was parsed with parse() instead of safeParse().

ZodError: [
  {
    "code": "invalid_type",
    "expected": "string",
    "received": "undefined",
    "path": [ "email" ],
    "message": "Required"
  }
]
    at get error (file:///app/node_modules/zod/lib/types.js:55:31)
    at ZodObject.parse (file:///app/node_modules/zod/lib/types.js:160:22)
    at createUser (file:///app/src/routes/users.js:14:31)

Quick fix

Read path and message in each issue: they name the field and the problem. For request bodies and other outside input, use safeParse() and return a 400 with error.issues (or error.flatten()) so a bad request is the client's error, not a crash. If the input is your own data, fix the producer or the schema.

  1. Look at path to find the field and code to see the kind of failure (invalid_type, too_small, invalid_string).
  2. For HTTP input, switch to safeParse() and answer 400 with the issues.
  3. For environment variables or config, keep parse() at startup so a bad value stops the boot with a clear message.
  4. If the shape changed on purpose, update the schema; use .optional() or .default() for fields that can be missing.
  5. Use z.coerce.number() for numbers that arrive as strings (query strings, form data).
typescript
import { z } from 'zod';

const NewUser = z.object({
  email: z.string().email(),
  age: z.coerce.number().int().min(13).optional(),
});

app.post('/users', (req, res) => {
  const result = NewUser.safeParse(req.body);
  if (!result.success) {
    return res.status(400).json({ error: 'invalid_body', issues: result.error.flatten().fieldErrors });
  }
  res.json(createUser(result.data));
});

Why it happens

parse() on untrusted input

Request bodies, query strings and webhook payloads come from outside. parse() throws on the first bad request and turns a client mistake into a 500.

A missing or renamed field

A client sending userEmail instead of email, or an API that dropped a field, gives invalid_type with received: undefined.

Numbers and booleans as strings

Query parameters and form fields are always strings, so z.number() rejects "42" unless you use z.coerce.number().

Schema and data drifting apart

A database column that became nullable, or an upstream API version change, makes parsing your own data fail after a deploy.

Other fixes

  • Log error.issues, not error.message, so the error tracker groups by field and code.
  • Share schemas between client and server so both sides agree on the shape.

Same error, other wording

  • ZodError: [ { "code": "too_small", "minimum": 1, "type": "string", "path": [ "name" ], "message": "String must contain at least 1 character(s)" } ]
  • ZodError: [ { "code": "invalid_string", "validation": "email", "path": [ "email" ], "message": "Invalid email" } ]
  • ZodError: [ { "expected": "number", "code": "invalid_type", "path": [ "page" ], "message": "Expected number, received string" } ]

Let your coding agent catch the next one

Runheed records this error in production with the stack trace, the breadcrumbs before it and the agent session that shipped the code, and serves it to Claude Code, Cursor or any MCP client. With it connected, you can ask:

> Find the ZodError exceptions in production, group them by path and code, tell me which come from client input (should be 400s) and which from our own data, and switch the input ones to safeParse.

Plans from $29/moSee what your agent readsSee the setupCancel any time from the billing portal

Related errors

References: Zod docs: formatting errors, Zod docs: basic usage (parse and safeParse)