Generate Zod schemas and TypeScript types from Rust types with zero runtime overhead and full type safety.
- Zero-cost abstractions - No runtime overhead, pure compile-time code generation
- Full type safety - End-to-end type safety from Rust to TypeScript
- Serde rename support - Automatic handling of
#[serde(rename = "...")]attributes - Derive macro support -
#[derive(ZodSchema)]for automatic schema generation - Primitive type support - Built-in support for all common Rust types
- Generic types - Automatic handling of
Option<T>,Vec<T>, and more - Custom schemas - Manual implementation for complex types
- Batch generation - Generate multiple schemas in a single TypeScript file
Add both crates to your Cargo.toml:
[dependencies]
zod_gen = "1.2.0"
zod_gen_derive = "1.2.0"use zod_gen::ZodSchema;
use zod_gen_derive::ZodSchema;
#[derive(ZodSchema)]
struct User {
id: u64,
name: String,
email: String,
is_admin: bool,
tags: Vec<String>,
profile: Option<UserProfile>,
}
#[derive(ZodSchema)]
struct UserProfile {
bio: String,
avatar_url: Option<String>,
}
#[derive(ZodSchema)]
enum UserStatus {
#[serde(rename = "active")]
Active,
#[serde(rename = "inactive")]
Inactive,
#[serde(rename = "suspended")]
Suspended,
}
fn main() {
// Generate Zod schema
println!("{}", User::zod_schema());
// Output: z.object({
// id: z.number(),
// name: z.string(),
// email: z.string(),
// is_admin: z.boolean(),
// tags: z.array(z.string()),
// profile: z.object({ bio: z.string(), avatar_url: z.string().optional() }).optional()
// })
}use zod_gen::{ZodSchema, zod_object, zod_string, zod_number, zod_boolean};
struct User {
id: u64,
name: String,
is_admin: bool,
}
impl ZodSchema for User {
fn zod_schema() -> String {
zod_object(&[
("id", zod_number()),
("name", zod_string()),
("is_admin", zod_boolean()),
])
}
}use zod_gen::ZodGenerator;
use std::fs;
fn generate_types() {
let mut generator = ZodGenerator::new();
// Add all your types with meaningful names
generator.add_schema::<User>("User");
generator.add_schema::<UserProfile>("UserProfile");
generator.add_schema::<UserStatus>("UserStatus");
// Generate a single TypeScript file with all schemas
let content = generator.generate();
fs::write("types/schemas.ts", content).unwrap();
}The generated TypeScript provides both Zod schemas and inferred types in a single file:
// Generated schemas.ts
import * as z from 'zod';
export const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string(),
is_admin: z.boolean(),
tags: z.array(z.string()),
profile: z.object({
bio: z.string(),
avatar_url: z.string().optional()
}).optional()
});
export type User = z.infer<typeof UserSchema>;
export const UserProfileSchema = z.object({
bio: z.string(),
avatar_url: z.string().optional()
});
export type UserProfile = z.infer<typeof UserProfileSchema>;
export const UserStatusSchema = z.union([z.literal('active'), z.literal('inactive'), z.literal('suspended')]);
export type UserStatus = z.infer<typeof UserStatusSchema>;Use it in your TypeScript code:
import { UserSchema, type User } from './types/schemas';
// Runtime validation
const validateUser = (data: unknown): User => {
return UserSchema.parse(data);
};
// Type-safe API calls
const createUser = async (user: User): Promise<User> => {
const response = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(user),
});
return UserSchema.parse(await response.json());
};This repository contains two crates:
zod_gen - Core Library
ZodSchematrait for defining schemas- Helper functions for building Zod expressions
ZodGeneratorfor batch file generation- Built-in implementations for primitive types
zod_gen_derive - Derive Macro
#[derive(ZodSchema)]procedural macro- Supports structs with named fields
- Supports enums with unit variants
- Automatic dependency resolution
String,&strβz.string()(TypeScript:string)i32,i64,u32,u64,f32,f64βz.number()(TypeScript:number)boolβz.boolean()(TypeScript:boolean)
Option<T>βT.optional()(TypeScript:Optional<T>)Vec<T>βz.array(T)(TypeScript:Array<T>)HashMap<String, T>βz.record(z.string(), T)(TypeScript:Record<string, T>)- Custom collections via manual implementation
- Named fields β
z.object({ ... }) - Nested structs supported
- Unit variants β
z.union([z.literal('A'), z.literal('B')]) - Serde rename support β
#[serde(rename = "custom_name")]βz.literal('custom_name')
zod_gen automatically handles #[serde(rename = "...")] attributes, ensuring your TypeScript types match your serialized JSON exactly:
use serde::{Serialize, Deserialize};
use zod_gen_derive::ZodSchema;
#[derive(ZodSchema, Serialize, Deserialize)]
enum ApiStatus {
#[serde(rename = "success")]
Success,
#[serde(rename = "error")]
Error,
#[serde(rename = "pending")]
Pending,
}
#[derive(ZodSchema, Serialize, Deserialize)]
enum UserRole {
#[serde(rename = "admin")]
Administrator,
#[serde(rename = "user")]
RegularUser,
#[serde(rename = "guest")]
GuestUser,
}Generated TypeScript:
export const ApiStatusSchema = z.union([z.literal('success'), z.literal('error'), z.literal('pending')]);
export type ApiStatus = z.infer<typeof ApiStatusSchema>;
export const UserRoleSchema = z.union([z.literal('admin'), z.literal('user'), z.literal('guest')]);
export type UserRole = z.infer<typeof UserRoleSchema>;Type Safety Benefits:
// β
TypeScript accepts the serde-renamed values
const status: ApiStatus = 'success';
const role: UserRole = 'admin';
// β TypeScript catches typos with the Rust variant names
const badStatus: ApiStatus = 'Success'; // Error: Type '"Success"' is not assignable to type 'ApiStatus'
const badRole: UserRole = 'Administrator'; // Error: Type '"Administrator"' is not assignable to type 'UserRole'This ensures perfect alignment between your Rust API and TypeScript frontend, catching serialization mismatches at compile time.
By default, zod_gen generates inline schemas for nested objects. This means that complex types are expanded directly into their Zod representation:
#[derive(ZodSchema)]
struct User {
id: u64,
name: String,
profile: Option<UserProfile>,
}
#[derive(ZodSchema)]
struct UserProfile {
bio: String,
avatar_url: Option<String>,
}
// Generates inline schema:
// z.object({
// id: z.number(),
// name: z.string(),
// profile: z.object({
// bio: z.string(),
// avatar_url: z.string().nullable()
// }).nullable()
// })This approach works consistently across all generic types:
use std::collections::HashMap;
// HashMap<String, User> generates:
// z.record(z.string(), z.object({
// id: z.number(),
// name: z.string(),
// profile: z.object({
// bio: z.string(),
// avatar_url: z.string().nullable()
// }).nullable()
// }))Benefits of inline schemas:
- β Self-contained - no external dependencies
- β Works with any nesting level
- β Consistent behavior across all types
- β No need to manage schema imports
Considerations:
- Schema duplication if the same type is used in multiple places
- Larger generated schemas for deeply nested structures
You provide the TypeScript type names when adding schemas to the generator:
let mut gen = ZodGenerator::new();
// Use meaningful names for your TypeScript types
gen.add_schema::<HashMap<String, User>>("UserMap");
gen.add_schema::<Vec<User>>("UserList");
gen.add_schema::<Option<UserProfile>>("OptionalProfile");This generates clean TypeScript with your chosen names:
export const UserMapSchema = z.record(z.string(), UserSchema);
export type UserMap = z.infer<typeof UserMapSchema>;
export const UserListSchema = z.array(UserSchema);
export type UserList = z.infer<typeof UserListSchema>;Benefits:
- Full control over TypeScript type names
- Meaningful names instead of auto-generated ones
- No magic - you decide what gets exported
- TypeScript types are inferred from Zod schemas using
z.infer<>
The ZodGenerator creates a single TypeScript file containing all your schemas. This approach:
- Simplifies file management - No need to track multiple files
- Reduces complexity - One file, one import
- Improves maintainability - All schemas in one place
- Enables tree-shaking - Import only what you need
If you need multiple files, simply create multiple generators:
// Generate API types
let mut api_gen = ZodGenerator::new();
api_gen.add_schema::<User>("User");
api_gen.add_schema::<Post>("Post");
std::fs::write("types/api.ts", api_gen.generate()).unwrap();
// Generate config types
let mut config_gen = ZodGenerator::new();
config_gen.add_schema::<AppConfig>("AppConfig");
std::fs::write("types/config.ts", config_gen.generate()).unwrap();use zod_gen::ZodSchema;
use chrono::{DateTime, Utc};
impl ZodSchema for DateTime<Utc> {
fn zod_schema() -> String {
"z.string().datetime()".to_string()
}
}Create a build.rs file:
use zod_gen::ZodGenerator;
fn main() {
let mut generator = ZodGenerator::new();
generator.add_schema::<MyType>("MyType");
// Generate during build
let content = generator.generate();
std::fs::write("frontend/types/schemas.ts", content).unwrap();
}Contributions are welcome! Please feel free to submit a Pull Request.
This project uses GitHub Actions for comprehensive automation:
-
π§ͺ Continuous Integration: Tests run on every PR and push
- Multi-version Rust testing (stable, beta, nightly)
- Code formatting and linting with clippy
- Documentation building and security audits
- Example validation and code coverage
-
π Automated Releases: Tag-triggered releases
- Automatic publishing to crates.io
- GitHub release creation with changelog
- Full validation before publishing
-
π Security & Maintenance:
- Weekly dependency updates via automated PRs
- Security vulnerability scanning
- Breaking change detection on PRs
-
π Documentation: Auto-deployed to GitHub Pages
- Follow Conventional Commits format
- Update
CHANGELOG.mdfor non-documentation changes - Ensure all tests pass and examples work
- Code must be formatted (
cargo fmt) and pass clippy lints
git clone https://github.com/cimatic/zod_gen.git
cd zod_gen
# The rust-toolchain.toml ensures you use the same Rust version as CI
cargo test --workspace
# Run clippy with CI-equivalent flags
./scripts/clippy-ci.shSee DEVELOPMENT.md for detailed development guidelines and CI consistency tips.
# Manual ZodSchema implementation
cargo run --example basic_usage
# Using the derive macro for automatic schema generation
cargo run --example derive_example
# Using ZodGenerator for multiple schemas with custom naming
cargo run --example generator_exampleThis project is licensed under the MIT License - see the LICENSE file for details.
- Zod - Runtime type validation for TypeScript
- serde - Inspiration for the derive macro pattern
- ts-rs - Alternative approach to RustβTypeScript codegen
Built with β€οΈ by the Cimatic team