In systems, there is the concept of constrained values. Some apps have fields designed to accept a specific set of predefined values. Examples of these include roles, statuses, and categories. Make sure to enter only these values to avoid system crashes and issues.
Zod is a TypeScript-first schema validation library. Its key features include simplicity, friendliness, error messages, and static type inference. Zod makes working with these constraints easier. It's used to define and validate schemas for use in data inputs, API responses, or forms. It simplifies coding by providing a well-tested set of validation utilities. The developer can focus on writing business logic.
In this article, we will venture into different types of Zod enums, such as Zod native enums, and Zod string enums. We will also look at validating enums with Zod, creating Zod enums from TypeScript enums, object keys, arrays, and much more.
Zod provides 2 basic ways to handle enums: z.enum() and z.nativeEnum(). Each has its use cases. z.enum()works with string-based enums while z.nativeEnum() works with Typescript native enums.
The basic z.enum() allows you to create a Zod schema that accepts a specific set of values. These values are often strings or numbers. Let us understand this through an example.
import { z } from "zod";
const colorEnum = z.enum(["red", "green", "blue"]);
Once defined this way, colorEnum can only accept the values "red", "green", or "blue". Attempts to pass in a value outside of this result in a validation error.
Zod.nativeEnum allows you to create a zod-enum schemas from typescripts native enum. It's handy especially if you are adding Zod to an existing project. In such scenarios, you might already have enums defined in typescript.
enum UserRole {
Admin = "ADMIN",
User = "USER",
Guest = "GUEST",
}
const userRoleSchema = z.nativeEnum(UserRole);
here is another example
enum Status {
Active = "active",
Inactive = "inactive",
Suspended = "suspended",
}
const statusSchema = z.nativeEnum(Status);
The Zod string enum presents a way to create an enum schema whose values are strictly strings. This is just another name for z.enum().
const categorySchema = z.enum(["Books", "Electronics", "Clothing"]);
Using Zod enums makes it easy to validate whether a value belongs to an enumerated set. You normally make use of .safeParse() or .parse() to validate values. Let's delve into both of them.
As its name suggests, safeParse() offers a safer alternative. safeParse () returns an object that contains validation information. It safely avoids exceptions at the cost of making your code more verbose. It's ideal when handling user input or external data sources. You cannot guarantee validity for such sources.
Here is some code:
const roleResult = RoleSchema.safeParse("guest");
if (!roleResult.success) {
console.log("Validation Error:", roleResult.error.errors);
} else {
console.log("Valid Role:", roleResult.data);
}
[Zod Validate Enum example]
Expected output:
Validation Error: [ { message: 'Invalid enum value...', path: [], ... }]
You would use parse when you are confident that the input is valid. This is because parse () will throw an error upon invalid input. It offers simplicity and directness but brings about extra work in dealing with the exceptions.
Here is the code:
import { z } from "zod";
const RoleSchema = z.enum(["admin", "editor", "viewer"]);
try {
const role = RoleSchema.parse("admin");
console.log("Valid role:", role); // Output: "Valid role: admin"
// This will throw an error
const invalidRole = RoleSchema.parse("guest");
} catch (error) {
console.error("Error:", error.errors[0].message);
}
[Zod Validate Enum example]
Here is the expected output;
Error: Invalid enum value. Expected 'admin' | 'editor' | 'viewer', received 'guest'
As we have seen, Zod is flexible and supports multiple ways to create the enums. Here, we'll cover creating Zod enums from diverse data types.
In Typescript, we use Objects to define mappings and also configurations. The objects are made up of key-value pairs which are potential enum candidates. This way, you get schemas that reflect your configuration and adapt to changes. Use when schemas need to strictly align with object definitions in your code. This is good for reducing redundancy and improving maintainability.
const permissions = {
READ: "read",
WRITE: "write",
DELETE: "delete",
};
const permissionsEnum = z.enum(Object.keys(permissions));
Zod also allows creating a zod array schema from the zod enum schema. Suppose you have an existing zod enum that defines a color schema. To create a zod array schema that validates an array to contain only the values in the enum, this is the right method. You can define arrays of enums in Zod to ensure each item in the array meets the enum’s constraints.
const colorEnum = z.enum(["red", "green", "blue"]);
const colorArraySchema = z.array(colorEnum);
const colors = colorArraySchema.safeParse(["red", "green", "blue"]);
Now, this validates that Every item in that array has to be one of the values in colorEnum (only "red," "green," or "blue").
Zod allows you to create a zod enum schema from an array. Arrays are a common data structure and having such a utility saves time and removes redundancy. This is a useful approach especially when working with dropdowns, options, tag lists, or any set of values that may evolve.
const dynamicValues = ["small", "medium", "large"] as const;
const sizeEnum = z.enum(dynamicValues);
Unions in Typescript define a variable that can hold one of several specified values. Zod also allows you to create enums from union types. This is useful when you want to enforce a specific set of strings but prefer using Typescripts unions for clearer intent and type inference.
import { z } from "zod";
// Define a union type of possible status values
type Status = "active" | "inactive" | "pending";
// Create a Zod enum schema using the union type
const statusEnum = z.enum(["active", "inactive", "pending"]);
// Example usage: validating a valid status
const result1 = statusEnum.safeParse("active");
console.log(result1.success); // ✅ true
// Example usage: validating an invalid status
const result2 = statusEnum.safeParse("archived");
console.log(result2.success); // ❌ false
We can also reverse the process and extract the values of an enum as an array. To do this we make use of the .options property.
import { z } from "zod";
// Define a Zod enum
const sizeEnum = z.enum(["small", "medium", "large"]);
// Convert the enum to an array using the .options property
const sizes = sizeEnum.options;
console.log(sizes); // ["small", "medium", "large"]
Dynamic enums can be created at runtime, offering flexibility with generated values. Consider the following example.
const dynamicValues = ["A", "B", "C"];
const dynamicEnum = z.enum(dynamicValues);
In addition to string-based enums, Zod also supports number-based enums. TO handle this we make use of z.nativeEnum() discussed above. Let us understand this through an example.
import { z } from "zod";
// Define a numeric enum in TypeScript
enum ResponseCode {
Success = 200,
NotFound = 404,
}
// Create a Zod schema for the numeric enum
const responseCodeSchema = z.nativeEnum(ResponseCode);
// Test validation with valid value
const validResponse = responseCodeSchema.safeParse(200);
console.log(validResponse.success); // ✅ true
// Test validation with invalid value
const invalidResponse = responseCodeSchema.safeParse(500);
console.log(invalidResponse.success); // ❌ false
console.log(invalidResponse.error.format()); // Error message for invalid value
When dealing with validation, providing meaningful validation error messages is important. The messages should be meaningful, user-friendly, and intuitive. Zod offers generic error messages which should be enough for common cases. However, for a better user experience, Zod allows custom error messages. We use .refine() to customize errors more effectively. The .refine() combines validation and customization of the error messages. Here is an example
import { z } from "zod";
// Define a simple enum for colors
const colorEnum = z.enum(["red", "green", "blue"]);
// Refine the enum to provide a custom error message
const colorEnumWithMessage = colorEnum.refine((val) => ["red", "green", "blue"].includes(val), {
message: "Color must be red, green, or blue.",
});
// Test validation
const validColor = colorEnumWithMessage.safeParse("red");
console.log(validColor.success); // ✅ true
const invalidColor = colorEnumWithMessage.safeParse("yellow");
console.log(invalidColor.success); // ❌ false
console.log(invalidColor.error.format()); // { message: "Color must be red, green, or blue." }
Zod's default validation error messages should be enough for most common cases. However, sometimes you want your users to understand what's wrong more clearly. For such cases, Zod offers the refine() method. .refine() is used to provide easier to understand validation messages .
z.enum() is used when you want to define a schema with a fixed set of known values (e.g., "open", "closed", "pending"). In contrast, z.union() creates a schema that accepts multiple distinct types.
z.union is more flexible. it allows you to combine different types e.g. strings and numbers. A simple rule of thumb would be to take z.enum for a set of strings and z.union when working with mixed types.
Zod offers the optional () method to mark fields that don't have to be filled at all times. Its useful to avoid validation errors when fields lack values.
Currently, you can’t directly attach descriptions to Zod enums. However, you can add an optional text field next to your enum field to store any descriptive information you want. This helps keep your data schema clear and informative. You can also include more context or details about what each enum value represents.
Zod handles object validation via schemas. For nested object validation, create corresponding nested validation schema.
Zod Enums works with arrays to let you create more complex validation rules. e.g. if you had an enum of statuses with values (open, closed, and pending) you can wrap them in z.array(). This validates that each item in an array matches one of these values. Zod also allows you to use array functions to limit the length using .min() and .max(). you can also use .refine() to ensure that all items are unique.
Zod enums and typescript enums differ in their usage. Zod enums are primarily used for data validation. They provide the validation rules for validating enums. Typescript enums are a language-specific type that defines a set of named constants.
Zod allows async logic inside validation. An example is when you have to wait for some asynchronous logic e.g. database call before validation. This is useful for complex validation where simple validation is not enough.
// Zod schema with async refinement
const RoleSchema = z
.string()
.refine((role) => RoleEnum.safeParse(role).success, {
message: "Invalid role",
})
.refine(async (role) => await checkRoleInDb(role), {
message: "Role does not exist in database",
});
//usage
const validateRole = async (role: string) => {
try {
await RoleSchema.parseAsync(role);
console.log("Valid role");
} catch (e) {
console.error("Validation error:", e.errors);
}
};
validateRole("ADMIN"); // Valid role
validateRole("UNKNOWN"); // Validation error: Role does not exist in database
Yes, Zod is flexible enough to allow Zod enums to work with third-party libraries. A good example of this is Zod nativeEnum() which works with Typescript's enum. Having the flex can avoid a lot of code duplication.
simplify and inspire technology
©2024, basicutils.com