Skip to content

Instantly share code, notes, and snippets.

@paulwongx
Last active July 21, 2024 14:26
Show Gist options
  • Save paulwongx/5e5506bcb64f0da6bde766b8080a2ea0 to your computer and use it in GitHub Desktop.
Save paulwongx/5e5506bcb64f0da6bde766b8080a2ea0 to your computer and use it in GitHub Desktop.
Zod Validation Errors

Zod Error formats

Table of Contents:

Given the following code:

const UserSchema = z.object({
	name: z.string(),
	email: z.string().email(),
	address: z.object({
		street: z.string(),
		city: z.string(),
	}),
});

type UserSchemaType = z.infer<typeof UserSchema>;

// Example usage
const result = UserSchema.safeParse({
	name: "",
	email: "invalid-email",
	address: {
		street: 453, // should be a string
		city: 123, // should be a string
	},
});

if (!result.success) {
	console.log("Zod error: ", JSON.stringify(result.error, null, 2));
	console.log("Zod error.format(): ", JSON.stringify(result.error.format(), null, 2));
	console.log("Zod error.errors: ", JSON.stringify(result.error.errors, null, 2));
	console.log("Zod error.flatten(): ", JSON.stringify(result.error.flatten(), null, 2));
}

result.error

{
  "issues": [
    {
      "validation": "email",
      "code": "invalid_string",
      "message": "Invalid email",
      "path": [
        "email"
      ]
    },
    {
      "code": "invalid_type",
      "expected": "string",
      "received": "number",
      "path": [
        "address",
        "street"
      ],
      "message": "Expected string, received number"
    },
    {
      "code": "invalid_type",
      "expected": "string",
      "received": "number",
      "path": [
        "address",
        "city"
      ],
      "message": "Expected string, received number"
    }
  ],
  "name": "ZodError"
}

result.error.format()

 {
  "_errors": [],
  "email": {
    "_errors": [
      "Invalid email"
    ]
  },
  "address": {
    "_errors": [],
    "street": {
      "_errors": [
        "Expected string, received number"
      ]
    },
    "city": {
      "_errors": [
        "Expected string, received number"
      ]
    }
  }
}

result.error.errors

// Returns an array
[
	{
		validation: "email",
		code: "invalid_string",
		message: "Invalid email",
		path: ["email"],
	},
	{
		code: "invalid_type",
		expected: "string",
		received: "number",
		path: ["address", "street"],
		message: "Expected string, received number",
	},
	{
		code: "invalid_type",
		expected: "string",
		received: "number",
		path: ["address", "city"],
		message: "Expected string, received number",
	},
];

result.error.flatten()

// Doesn't work well with nested fields
{
  "formErrors": [],
  "fieldErrors": {
    "email": [
      "Invalid email"
    ],
    "address": [
      "Expected string, received number",
      "Expected string, received number"
    ]
  }
}

Custom Format

Function

// Inspired from Next-Safe-Action
import { z } from "zod";
type ObjectLiteral = Record<string, any>;
type ValidationErrors<TSchema extends ObjectLiteral | undefined> = {
	[K in keyof TSchema]?: TSchema[K] extends ObjectLiteral ? ValidationErrors<TSchema[K]> : string[];
} & {
	_errors?: string[];
};

export function formatZodError<TSchema extends ObjectLiteral | undefined>(
	zodError: z.ZodError
): ValidationErrors<TSchema> {
	const validationErrors: ValidationErrors<TSchema> = {};

	for (const issue of zodError.issues) {
		let current: any = validationErrors;

		for (let i = 0; i < issue.path.length; i++) {
			const key = issue.path[i];
			if (typeof key !== "string" && typeof key !== "number") continue;

			if (i === issue.path.length - 1) {
				// We're at the leaf of the path
				if (!current[key]) {
					current[key] = [];
				}
				if (Array.isArray(current[key])) {
					current[key].push(issue.message);
				}
			} else {
				// We're still traversing the path
				if (!current[key] || typeof current[key] !== "object") {
					current[key] = {};
				}
				current = current[key];
			}
		}

		// Handle root-level errors
		if (issue.path.length === 0) {
			if (!validationErrors._errors) {
				validationErrors._errors = [];
			}
			validationErrors._errors.push(issue.message);
		}
	}

	return validationErrors;
}

Custom Error Format Output

if (!result.success) {
	const validationErrors = zodErrorToValidationErrors<UserSchemaType>(result.error);
	console.log("Custom error: ", validationErrors);
}

Output:

{
	"email": ["Invalid email"],
	"address": {
		"street": ["Expected string, received number"],
		"city": ["Expected string, received number"]
	}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment