0

I'm building a microservice with Moleculer.js and TypeScript, using Zod v4 for schema validation. I need to validate API responses that return null for optional fields, while keeping strict validation for request payloads.

My API returns employee data with null values for optional fields:

{
  "firstName": "John",
  "lastName": "Doe",
  "externalRefId": null,        // null from API
  "profilePhotoUrl": null,      // null from API
  "countryOfBirth": null,       // null from API
  "employeeStatus": null,       // null from API
  // ... more fields with null
}

My base schema uses .optional():

export const EmployeeSchema = z.object({
  id: z.string().optional(),
  firstName: z.string(),
  lastName: z.string(),
  externalRefId: z.string().optional(),
  profilePhotoUrl: z.string().optional(),
  employeeStatus: EmployeeStatusSchema.optional(),
  // ... 20+ more fields
});

When I validate responses, I get errors:

 {
  "message": "Validation failed",
  "errors": [
    {
      "field": "externalRefId",
      "code": "invalid_type",
      "message": "Invalid input: expected string, received null"
    },
    {
      "field": "profilePhotoUrl",
      "code": "invalid_type",
      "message": "Invalid input: expected string, received null"
    }
    // ... more fields
  ]
}

I've Tried

  1. Using .nullish() on the entire schema - doesn't work, fields still reject null:

    export const CreateEmployeeResponseSchema = EmployeeSchema.nullish();
    // Still fails validation
    
  2. Making base fields nullable, then removing nullable for requests:

    // Made base schema nullable
    export const EmployeeSchema = z.object({
      externalRefId: z.string().nullable(),
      profilePhotoUrl: z.string().nullable(),
      // ...
    });
    
    // Tried to make strict for requests by extending
    export const CreateEmployeeRequestSchema = EmployeeSchema.extend({
      firstName: z.string().min(1),
      // But can't "remove" nullable from inherited fields
    });
    // Problem: Request schema still accepts null values
    

Requirements

  • Request validation: Must enforce required fields (no null allowed)

  • Response validation: Must accept null values from API

  • DRY: Avoid duplicating the entire 30+ field schema manually

Current Service Structure:

// employee.models.ts
export const CreateEmployeeRequestSchema = EmployeeSchema.extend({
  firstName: z.string().min(1, "First name is required"),
  // ... strict validation
});

export const CreateEmployeeResponseSchema = EmployeeSchema.nullish();

// employee.helpers.ts
async createEmployee(payload: CreateEmployeeRequest): Promise<CreateEmployeeResponse | null> {
    const employee = await this.tcatEmployeeProxy.createEmployee(payload);
    return validateSchema(CreateEmployeeResponseSchema, employee); // Fails here
}

Environment:

  • Zod: v4 (latest)

  • TypeScript: 5.x

  • Moleculer: Latest

  • Node.js: 18+

What's the recommended approach in Zod v4 to:

  1. Reuse a base schema for both strict input validation and flexible output validation?

  2. Handle API responses with null values without manually redefining all fields?

  3. Remove nullable constraints when extending schemas for stricter validation?

  4. Maintain type safety for both input and output types?

Is there a utility function or pattern in Zod v4 that can convert all .optional() fields to .nullish() for response schemas, or remove .nullable() from fields for request schemas?

3
  • Reminder: Answers generated by AI tools are not allowed due to Stack Overflow's artificial intelligence policy Commented Nov 12 at 12:39
  • The first thing that comes to mind whenever I'm asked "how do I avoid repeating myself?" is: factor out a function. Would you find this acceptable as an answer? Commented Nov 12 at 13:08
  • Your list of questions at the end makes it seem like you're asking multiple things, but really it looks like the same question asked in different ways. I'd suggest editing to sum up your question in a single well-formed sentence to avoid the risk of this being closed as "needs more focus". Commented Nov 12 at 13:23

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.