Enhancing Type Safety and Validation with Zod: Use Cases, OpenAPI Integration, and Comparison with Joi
Data validation is critical to software development, especially in TypeScript-based applications where type safety and runtime validation go hand-in-hand. One powerful library that shines in this regard is Zod, a TypeScript-first schema declaration and validation library. Zod helps developers define schemas for validation, generate types, and enforce runtime safety.
In this blog post, we’ll explore the importance of Zod, its use cases, how it helps with schema validation and type generation, and how it compares to other libraries like Joi. We’ll also cover an advanced topic: automatically generating Zod schemas from OpenAPI specifications using the typed-openapi
package.
What is Zod?
Zod is a TypeScript-centric validation library that enables developers to define and validate schemas, infer types from schemas, and ensure runtime validation. While there are other popular libraries, Zod stands out for its simplicity, flexibility, and tight integration with TypeScript.
Key Features of Zod:
- TypeScript-first: Zod is built with TypeScript in mind. It automatically infers static types from schemas, reducing boilerplate code.
- Zero dependencies: Lightweight and efficient.
- Chainable methods: Zod allows developers to chain validations for cleaner and more readable code.
- Synchronous and asynchronous validation: Supports both sync and async operations out of the box.
The Importance of Zod Schema
1. Schema Validation
One of Zod’s primary features is schema validation. It allows you to define the structure and shape of your data and ensures that it adheres to that shape at runtime. Here’s a quick example of how Zod works for schema validation:
import { z } from "zod";
const userSchema = z.object({
name: z.string(),
age: z.number().int().min(18),
email: z.string().email(),
});
const validUser = userSchema.safeParse({
name: "John Doe",
age: 25,
email: "john.doe@example.com",
});
2. Type Inference
One of Zod's most powerful features is its seamless integration with TypeScript for type inference. When you define a schema in Zod, you don’t need to define TypeScript types for your data separately. Zod automatically generates the types for you, which ensures that your static and runtime types are always in sync.
import { z } from 'zod';
// Define a Zod schema
const userSchema = z.object({
name: z.string(),
age: z.number().int().min(18),
email: z.string().email(),
isAdmin: z.boolean().optional(), // Optional field
});
// Infer the TypeScript type from the Zod schema
type User = z.infer<typeof userSchema>;
// Example of using the inferred type
const createUser = (user: User) => {
console.log(`User Name: ${user.name}, Age: ${user.age}, Email: ${user.email}`);
};
// Use the createUser function with valid data
const newUser = {
name: "Jane Doe",
age: 30,
email: "jane.doe@example.com",
isAdmin: false,
};
createUser(newUser); // Works because the data matches the inferred User type
3. Runtime Validation in TypeScript
TypeScript provides type safety at compile-time, but it doesn’t offer runtime guarantees. Zod fills this gap by providing runtime validation to ensure the integrity of your data, especially when dealing with untrusted sources.
import { z } from "zod";
// Define a Zod schema for user validation
const userSchema = z.object({
name: z.string(),
age: z.number().min(18, "User must be at least 18 years old"),
email: z.string().email(),
});
// Example of untrusted data (e.g., from an API or form input)
const userInput = {
name: "John Doe",
age: 16, // Invalid age (below 18)
email: "johndoe.com" // Invalid email format
};
// Validate the input using Zod's `safeParse` method for runtime validation
const result = userSchema.safeParse(userInput);
if (result.success) {
console.log("Valid User:", result.data);
} else {
// If validation fails, log the errors
console.error("Validation Errors:", result.error.errors);
}
// Handling Validation Results: If validation succeeds, we log the valid user data; if it fails, we handle and log the specific validation errors.
// Output:
Validation Errors: [
{ "path": ["age"], "message": "User must be at least 18 years old" },
{ "path": ["email"], "message": "Invalid email" }
]
4. Schema Transformation
Zod allows schema transformation, enabling you to not only validate your data but also transform it into the desired format.
import { z } from "zod";
// Define a Zod schema with transformation
const userSchema = z
.object({
firstName: z.string(),
lastName: z.string(),
birthYear: z.number().int().min(1900).max(new Date().getFullYear()), // Ensure valid year
email: z.string().email(),
})
.transform((data) => {
// Transform the validated data
const age = new Date().getFullYear() - data.birthYear;
return {
...data,
fullName: `${data.firstName} ${data.lastName}`,
age,
};
});
// Example of input data
const userInput = {
firstName: "Jane",
lastName: "Doe",
birthYear: 1990,
email: "jane.doe@example.com",
};
// Validate and transform the input data
const result = userSchema.safeParse(userInput);
if (result.success) {
// Log the transformed data
console.log("Transformed User Data:", result.data);
} else {
// Handle validation errors
console.error("Validation Errors:", result.error.errors);
}
Explanation:
- This schema validates a user object with fields
firstName
,lastName
,birthYear
, andemail
. It also includes a transformation step.
Output:
{
"firstName": "Jane",
"lastName": "Doe",
"birthYear": 1990,
"email": "jane.doe@example.com",
"fullName": "Jane Doe",
"age": 34
}
Use Cases of Zod
1. API Request/Response Validation
Validating data received from external API requests is a common use case. Since you have no control over the data returned by third-party APIs, it’s essential to validate the responses before proceeding.
Example: Validating an External API Response with Zod
Here’s an example where we fetch user data from an API and validate the response using a Zod schema:
import { z } from "zod";
const userSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
isActive: z.boolean(),
});
async function fetchAndValidateUser() {
try {
const response = await fetch("https://api.example.com/user/123");
if (!response.ok) {
throw new Error("Failed to fetch user data");
}
const data = await response.json();
const result = userSchema.safeParse(data);
if (result.success) {
console.log("Validated User Data:", result.data);
} else {
console.error("Validation Errors:", result.error.errors);
}
} catch (error) {
console.error("Error during API call:", error);
}
}
fetchAndValidateUser();
This ensures that only valid, well-structured data is processed, safeguarding your application from runtime errors due to unexpected data.
2. Form Validation
Zod is great for validating user input in forms, particularly in frameworks like React.
3. Configuration Validation
When working with configuration files, Zod helps validate that all necessary fields are present and properly formatted.
4. Data Validation for Third-party APIs
When interacting with third-party APIs, Zod ensures the responses are correctly formatted and reliable before they are processed.
Zod vs Joi: How is Zod Different?
Both Zod and Joi are popular schema validation libraries, but they cater to different use cases.
Key Differences:
- TypeScript-first Approach: Zod is tightly integrated with TypeScript, while Joi is a JavaScript library with less TypeScript support.
- API Design: Zod offers a chainable, cleaner API compared to Joi’s declarative syntax.
- Zero Dependencies: Zod is lightweight and has zero dependencies, making it fast and efficient.
- Validation Speed: Zod’s simplicity generally leads to faster validation.
Going One Step Further: Generating Zod Schemas from OpenAPI Using typed-openapi
Manually defining Zod schemas for each API endpoint can become tedious, especially when working with large APIs. If you’re working with APIs that are documented with OpenAPI (formerly known as Swagger), you can automatically generate Zod schemas from OpenAPI specifications using the typed-openapi package.
What is typed-openapi
?
typed-openapi
is a tool that can generate TypeScript types and Zod schemas based on OpenAPI specifications. It simplifies the process of integrating third-party APIs by ensuring that the types and validation schemas stay consistent with the API definition.
Benefits of Using typed-openapi
:
- Eliminate Manual Work: Automatically generate Zod schemas from OpenAPI specs without manually writing validation logic.
- Consistency: Ensure that your Zod schemas match the OpenAPI documentation, reducing bugs caused by mismatched data structures.
- Type-Safe Validation: Leverage TypeScript’s type inference alongside runtime validation.
Example: Using typed-openapi
to Generate Zod Schemas
Let’s walk through how you can use typed-openapi
to automatically generate Zod schemas from an OpenAPI specification.
Step 1: Install typed-openapi
First, you’ll need to install the package:
npm install typed-openapi
Step 2: Generate Zod Schemas from OpenAPI
Once the package is installed, you can use the CLI to generate TypeScript types and Zod schemas from your OpenAPI specification. Here’s an example of how to do this:
execSync(
'typed-openapi ./schemas/openapi.yaml -r zod --output ./generated/types.schema.zod.ts',
{
stdio: 'inherit',
cwd: join(__dirname, '../src/_generated'),
},
);
This command reads the OpenAPI specification (openapi.yaml
), and generates TypeScript types and Zod validation schemas into the ./generated
folder.
Step 3: Import and Use Generated Zod Schemas
After generating the types and Zod schemas, you can use them directly in your application. For example, if your OpenAPI schema defines a User
object, typed-openapi
will generate the corresponding Zod schema for you.
Here’s an example of how you might use the generated schemas:
import { z } from "zod";
import { schemas } from "./generated"; // Import the generated schemas
// Assuming 'User' is a schema in your OpenAPI definition
const userSchema = schemas.User;
async function fetchAndValidateUser() {
try {
const response = await fetch("https://api.example.com/user/123");
const data = await response.json();
// Validate the response using the generated Zod schema
const result = userSchema.safeParse(data);
if (result.success) {
console.log("Validated User Data:", result.data);
} else {
console.error("Validation Errors:", result.error.errors);
}
} catch (error) {
console.error("Error during API call:", error);
}
}
fetchAndValidateUser();
In this example:
- The
userSchema
is generated directly from the OpenAPI specification usingtyped-openapi
. - You use this schema to validate the API response, ensuring that the data adheres to the expected structure.
Benefits of Using Typed OpenAPI with Zod
- Automatic Schema Generation: No need to manually write and update validation schemas. This is particularly useful when the API schema changes frequently.
- Error Reduction: Automatically generated schemas prevent potential bugs and reduce the likelihood of mismatched schemas between the API and your validation logic.
- Time Efficiency: Saves time when working with large and complex APIs by automating schema generation.
Conclusion
Zod is a powerful, TypeScript-first schema validation library that helps ensure data integrity, type safety, and cleaner code. Whether you’re working with form validation, API response validation, or configuration management, Zod makes data validation easy and reliable.