Published on

Fixing Zod 'Expected String, Received Undefined' Error in TypeScript

Authors
  • Name
    Ripal & Zalak
    Twitter

Fixing Zod 'Expected String, Received Undefined' Error in TypeScript

When working with Zod for schema validation in a TypeScript application, you might encounter the error:

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

This error occurs when Zod expects a string but receives undefined. In this post, we'll explore why this happens and how to resolve it effectively.

Problem

The error arises when the request body (req.body) doesn't contain the required fields, or they are improperly formatted. This is often due to:

  • A missing or misconfigured middleware to parse request bodies (e.g., express.json() for JSON payloads).
  • Invalid or incomplete data sent in the request.

Example Scenario

Here’s a typical example where this error might occur:

Prisma Model

model Client {
  id         String          @id @default(uuid())
  email      String          @unique
  name       String
  password   String
  created_at DateTime        @default(now())
  updated_at DateTime        @updatedAt
  reviews    ProductReview[] @relation("client")
  adm        Boolean         @default(false)

  @@map("clients")
}

Express Route and Controller

Route:

const authController = new AuthController()
router.route('/register').post(authController.registerClient)

Controller:

import { z } from 'zod'
import { Request, Response } from 'express'

export class AuthController {
  async registerClient(req: Request, res: Response) {
    const createClientBody = z.object({
      name: z.string(),
      email: z.string(),
      password: z.string(),
    })

    const { name, email, password } = createClientBody.parse(req.body)

    const response = await prisma.client.create({
      data: { name, email, password },
    })

    return res.status(201).send({ response })
  }
}

Error Example

Sending a request with an empty body ({}) results in:

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

Solution

Step 1: Add Middleware for Body Parsing

Ensure that your Express app includes the correct middleware to parse JSON request bodies:

import express from 'express'
const app = express()
app.use(express.json())

Without this middleware, req.body will be empty, causing Zod validation to fail.

Step 2: Validate and Handle Missing Fields Gracefully

To make the schema more robust, allow fields to be optional or handle missing values:

const createClientBody = z.object({
  name: z.string().optional().default(''),
  email: z.string().optional().default(''),
  password: z.string().optional().default(''),
})

Alternatively, use .nullish() for nullable or undefined values:

const createClientBody = z.object({
  name: z.string().nullish(),
  email: z.string().nullish(),
  password: z.string().nullish(),
})

Step 3: Debug Incoming Data

Log req.body before parsing to confirm the payload is being sent correctly:

console.log(req.body)

Step 4: Use Proper Error Handling

Wrap the validation in a try-catch block to provide meaningful error responses:

try {
  const { name, email, password } = createClientBody.parse(req.body)
  const response = await prisma.client.create({
    data: { name, email, password },
  })
  res.status(201).send({ response })
} catch (e) {
  if (e instanceof z.ZodError) {
    res.status(400).send({ errors: e.errors })
  } else {
    res.status(500).send({ error: 'Internal server error' })
  }
}

FAQ

1. Why is req.body empty?

This happens when the express.json() middleware is not added to your Express app. Add it as shown in Step 1.

2. What does .nullish() do in Zod?

The .nullish() method allows a field to accept null or undefined values in addition to the expected type.

3. How can I ensure all required fields are present?

Use the default z.string() schema and provide clear error messages when fields are missing:

z.string({ required_error: 'This field is required' })

4. Can I provide default values?

Yes! Use .default(value) to provide default values for optional fields:

z.string().optional().default('Default Value')