Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Generate Zod schemas and TypeScript types from Rust types. Use with zod_gen_derive for automatic #[derive(ZodSchema)] support and serde rename compatibility.

License

Notifications You must be signed in to change notification settings

cimatic/zod_gen

Repository files navigation

zod_gen

Crates.io Documentation License: MIT CI codecov

Generate Zod schemas and TypeScript types from Rust types with zero runtime overhead and full type safety.

πŸš€ Features

  • 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

πŸ“¦ Installation

Add both crates to your Cargo.toml:

[dependencies]
zod_gen = "1.2.0"
zod_gen_derive = "1.2.0"

πŸ”§ Quick Start

Using the derive macro (recommended)

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()
 // })
}

Manual implementation

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()),
 ])
 }
}

Single file generation with ZodGenerator

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();
}

πŸ“š Generated TypeScript

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());
};

πŸ—οΈ Architecture

This repository contains two crates:

zod_gen - Core Library

  • ZodSchema trait for defining schemas
  • Helper functions for building Zod expressions
  • ZodGenerator for 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

🎯 Supported Types

Primitives

  • String, &str β†’ z.string() (TypeScript: string)
  • i32, i64, u32, u64, f32, f64 β†’ z.number() (TypeScript: number)
  • bool β†’ z.boolean() (TypeScript: boolean)

Generics

  • 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

Structs

  • Named fields β†’ z.object({ ... })
  • Nested structs supported

Enums

  • Unit variants β†’ z.union([z.literal('A'), z.literal('B')])
  • Serde rename support β†’ #[serde(rename = "custom_name")] β†’ z.literal('custom_name')

🎯 Serde Rename Support

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.

πŸ”§ Advanced Usage

Schema Generation Strategy

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

User-Controlled Naming

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<>

Single File Output

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();

Custom Schema Implementation

use zod_gen::ZodSchema;
use chrono::{DateTime, Utc};
impl ZodSchema for DateTime<Utc> {
 fn zod_schema() -> String {
 "z.string().datetime()".to_string()
 }
}

Integration with Build Scripts

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();
}

🀝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

πŸ”„ CI/CD Pipeline

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

πŸ“‹ PR Guidelines

  • Follow Conventional Commits format
  • Update CHANGELOG.md for non-documentation changes
  • Ensure all tests pass and examples work
  • Code must be formatted (cargo fmt) and pass clippy lints

Development Setup

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.sh

See DEVELOPMENT.md for detailed development guidelines and CI consistency tips.

Running Examples

# 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_example

πŸ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.

πŸ™ Acknowledgments

  • 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

About

Generate Zod schemas and TypeScript types from Rust types. Use with zod_gen_derive for automatic #[derive(ZodSchema)] support and serde rename compatibility.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

AltStyle γ«γ‚ˆγ£γ¦ε€‰ζ›γ•γ‚ŒγŸγƒšγƒΌγ‚Έ (->γ‚ͺγƒͺγ‚ΈγƒŠγƒ«) /