| Crates.io | zod-rs |
| lib.rs | zod-rs |
| version | 0.4.0 |
| created_at | 2025-06-29 08:45:59.877109+00 |
| updated_at | 2026-01-17 06:32:17.944673+00 |
| description | TypeScript Zod-inspired schema validation library for Rust with static type inference |
| homepage | https://github.com/maulanasdqn/zod-rs |
| repository | https://github.com/maulanasdqn/zod-rs |
| max_upload_size | |
| id | 1730552 |
| size | 131,330 |
๐ฆ A Rust implementation inspired by Zod for schema validation
zod-rs is a TypeScript-first schema validation library with static type inference, inspired by Zod. It provides a simple and intuitive API for validating JSON data with comprehensive error reporting.
#[zod(...)] attributesvalidator crateAdd zod-rs to your Cargo.toml:
[dependencies]
zod-rs = "0.1.0"
# Optional: for web framework integration
zod-rs = { version = "0.1.0", features = ["axum"] }
# For schema derivation from structs (recommended)
zod-rs = "0.1.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
use serde_json::json;
use zod_rs::prelude::*;
fn main() {
// Define a schema
let user_schema = object()
.field("name", string().min(2).max(50))
.field("email", string().email())
.field("age", number().min(0.0).max(120.0).int());
// Validate data
let user_data = json!({
"name": "Alice",
"email": "alice@example.com",
"age": 25
});
match user_schema.safe_parse(&user_data) {
Ok(validated_data) => println!("โ
Valid: {:?}", validated_data),
Err(errors) => println!("โ Invalid: {}", errors),
}
}
use zod_rs::prelude::*;
use serde_json::json;
// Basic string
let schema = string();
assert!(schema.safe_parse(&json!("hello")).is_ok());
// String with length constraints
let schema = string().min(3).max(10);
assert!(schema.safe_parse(&json!("hello")).is_ok());
assert!(schema.safe_parse(&json!("hi")).is_err());
// Exact length
let schema = string().length(5);
assert!(schema.safe_parse(&json!("hello")).is_ok());
// Pattern matching
let schema = string().regex(r"^[a-zA-Z]+$");
assert!(schema.safe_parse(&json!("hello")).is_ok());
assert!(schema.safe_parse(&json!("hello123")).is_err());
// Email validation
let schema = string().email();
assert!(schema.safe_parse(&json!("user@example.com")).is_ok());
// URL validation
let schema = string().url();
assert!(schema.safe_parse(&json!("https://example.com")).is_ok());
use zod_rs::prelude::*;
use serde_json::json;
// Basic number
let schema = number();
assert!(schema.safe_parse(&json!(42.5)).is_ok());
// Integer only
let schema = number().int();
assert!(schema.safe_parse(&json!(42)).is_ok());
assert!(schema.safe_parse(&json!(42.5)).is_err());
// Range constraints
let schema = number().min(0.0).max(100.0);
assert!(schema.safe_parse(&json!(50)).is_ok());
assert!(schema.safe_parse(&json!(-1)).is_err());
// Positive numbers
let schema = number().positive();
assert!(schema.safe_parse(&json!(1)).is_ok());
assert!(schema.safe_parse(&json!(0)).is_err());
// Non-negative numbers
let schema = number().nonnegative();
assert!(schema.safe_parse(&json!(0)).is_ok());
assert!(schema.safe_parse(&json!(-1)).is_err());
// Finite numbers (excludes NaN, Infinity)
let schema = number().finite();
assert!(schema.safe_parse(&json!(42.0)).is_ok());
use zod_rs::prelude::*;
use serde_json::json;
let schema = boolean();
assert!(schema.safe_parse(&json!(true)).is_ok());
assert!(schema.safe_parse(&json!(false)).is_ok());
assert!(schema.safe_parse(&json!("true")).is_err());
use zod_rs::prelude::*;
use serde_json::json;
// String literal
let schema = literal("active".to_string());
assert!(schema.safe_parse(&json!("active")).is_ok());
assert!(schema.safe_parse(&json!("inactive")).is_err());
// Number literal
let schema = literal(42.0);
assert!(schema.safe_parse(&json!(42.0)).is_ok());
assert!(schema.safe_parse(&json!(43.0)).is_err());
use zod_rs::prelude::*;
use serde_json::json;
// Array of strings
let schema = array(string());
assert!(schema.safe_parse(&json!(["a", "b", "c"])).is_ok());
// Array with length constraints
let schema = array(string()).min(1).max(5);
assert!(schema.safe_parse(&json!(["a"])).is_ok());
assert!(schema.safe_parse(&json!([])).is_err());
// Array with exact length
let schema = array(number()).length(3);
assert!(schema.safe_parse(&json!([1, 2, 3])).is_ok());
// Nested arrays
let schema = array(array(string()));
assert!(schema.safe_parse(&json!([["a", "b"], ["c", "d"]])).is_ok());
use zod_rs::prelude::*;
use serde_json::json;
// Simple object
let schema = object()
.field("name", string())
.field("age", number());
let data = json!({"name": "Alice", "age": 25});
assert!(schema.safe_parse(&data).is_ok());
// Object with optional fields
let schema = object()
.field("name", string())
.optional_field("bio", string());
let data = json!({"name": "Alice"});
assert!(schema.safe_parse(&data).is_ok());
// Strict mode (no additional properties)
let schema = object()
.field("name", string())
.strict();
use zod_rs::prelude::*;
use serde_json::json;
let schema = optional(string());
assert!(schema.safe_parse(&json!(null)).is_ok());
assert!(schema.safe_parse(&json!("hello")).is_ok());
// Method chaining
let schema = string().optional();
use zod_rs::prelude::*;
use serde_json::json;
let schema = union()
.variant(string())
.variant(number());
assert!(schema.safe_parse(&json!("hello")).is_ok());
assert!(schema.safe_parse(&json!(42)).is_ok());
assert!(schema.safe_parse(&json!(true)).is_err());
// Literal unions (enums)
let schema = union()
.variant(literal("small".to_string()))
.variant(literal("medium".to_string()))
.variant(literal("large".to_string()));
All schemas support these methods:
parse(value) - Parse with panic on errorlet schema = string();
let result = schema.parse(&json!("hello")); // Panics on validation failure
safe_parse(value) - Parse with Resultlet schema = string();
match schema.safe_parse(&json!("hello")) {
Ok(value) => println!("Valid: {}", value),
Err(errors) => println!("Invalid: {}", errors),
}
validate(value) - Alias for safe_parselet schema = string();
let result = schema.validate(&json!("hello"));
use serde::{Deserialize, Serialize};
use serde_json::json;
use zod_rs::prelude::*;
#[derive(Debug, Serialize, Deserialize)]
struct User {
username: String,
email: String,
age: f64,
interests: Vec<String>,
}
fn user_schema() -> impl Schema<Value> {
object()
.field("username", string().min(3).max(20).regex(r"^[a-zA-Z0-9_]+$"))
.field("email", string().email())
.field("age", number().min(13.0).max(120.0).int())
.field("interests", array(string()).min(1).max(10))
}
fn main() {
let user_data = json!({
"username": "alice_dev",
"email": "alice@example.com",
"age": 28,
"interests": ["rust", "programming"]
});
// Validate and deserialize
match user_schema().validate(&user_data) {
Ok(_) => {
let user: User = serde_json::from_value(user_data).unwrap();
println!("Valid user: {:?}", user);
}
Err(errors) => {
println!("Validation failed: {}", errors);
}
}
}
use zod_rs::prelude::*;
use serde_json::json;
fn address_schema() -> impl Schema<Value> {
object()
.field("street", string().min(1))
.field("city", string().min(1))
.field("country", string().length(2)) // ISO country code
.field("zip", string().regex(r"^\d{5}(-\d{4})?$"))
}
fn user_schema() -> impl Schema<Value> {
object()
.field("name", string())
.field("email", string().email())
.field("address", address_schema())
.optional_field("billing_address", address_schema())
}
let user_data = json!({
"name": "John Doe",
"email": "john@example.com",
"address": {
"street": "123 Main St",
"city": "Boston",
"country": "US",
"zip": "02101"
}
});
assert!(user_schema().safe_parse(&user_data).is_ok());
Enable the axum feature in your Cargo.toml:
[dependencies]
zod-rs = { version = "0.1.0", features = ["axum"] }
axum = "0.7"
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
use axum::{
extract::Json,
http::StatusCode,
response::{IntoResponse, Json as ResponseJson},
routing::post,
Router,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use zod_rs::prelude::*;
#[derive(Debug, Serialize, Deserialize)]
struct CreateUserRequest {
name: String,
email: String,
age: f64,
}
#[derive(Serialize)]
struct ApiResponse<T> {
success: bool,
data: Option<T>,
errors: Option<Vec<String>>,
}
fn user_schema() -> impl Schema<Value> {
object()
.field("name", string().min(2).max(50))
.field("email", string().email())
.field("age", number().min(13.0).max(120.0).int())
}
async fn create_user(Json(payload): Json<Value>) -> impl IntoResponse {
match user_schema().validate(&payload) {
Ok(_) => {
let user: CreateUserRequest = serde_json::from_value(payload).unwrap();
(
StatusCode::CREATED,
ResponseJson(ApiResponse {
success: true,
data: Some(user),
errors: None,
}),
)
}
Err(validation_result) => {
let errors: Vec<String> = validation_result
.issues
.iter()
.map(|issue| issue.to_string())
.collect();
(
StatusCode::BAD_REQUEST,
ResponseJson(ApiResponse::<CreateUserRequest> {
success: false,
data: None,
errors: Some(errors),
}),
)
}
}
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/users", post(create_user));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
zod-rs provides detailed error information with path tracking:
use zod_rs::prelude::*;
use serde_json::json;
let schema = object()
.field("user", object()
.field("name", string().min(2))
.field("email", string().email())
);
let invalid_data = json!({
"user": {
"name": "A",
"email": "invalid-email"
}
});
match schema.safe_parse(&invalid_data) {
Err(errors) => {
println!("{}", errors);
// Output:
// - user.name: Too big: expected string to have >= 2 characters
// - user.email: Invalid email address
}
_ => {}
}
ValidationError::Required - Missing required fieldValidationError::InvalidType - Wrong data typeValidationError::InvalidValue - Provided value does not match the expected valueValidationError::InvalidValues - Provided value does not match any of the expected values.ValidationError::TooSmall / TooBig - Value or lenght out of range (String, Number, Array ... etc)ValidationError::InvalidFormat - String format validation (starts with , ends with, includes, regex, ... etc)ValidationError::InvalidNumber - Invalid number constraint (finite, positive, ... etc)ValidationError::UnrecognizedKeys - Object with unrecognized keysValidationError::InvalidUnion - No union matchingValidationError::Custom - Custom validation errorszod-rs comes with built-in locale support so you can get validation errors in different languages.
Example
use serde_json::json;
use zod_rs::prelude::*;
let login_schema = object()
.field("email", string().ends_with("@domain.com"))
.field("password", string().min(8))
.strict();
let input = json!({
"email": "john@example.com",
"password": "Strongpassword123"
});
match login_schema.safe_parse(&input) {
Ok(output) => println!("โ
Valid login: {output}"),
Err(err) => println!("{}", err.local(Locale::Ar)),
}
๐ก Want to add a new language? Missing a translation? Open an issue or PR on GitHub โ contributions are welcome.
zod-rs provides a powerful derive macro that automatically generates validation schemas from Rust structs, making it an excellent replacement for the validator crate.
use serde::{Deserialize, Serialize};
use serde_json::json;
use zod_rs::prelude::*;
#[derive(Debug, Serialize, Deserialize, ZodSchema)]
struct User {
#[zod(min_length(3), max_length(20), regex(r"^[a-zA-Z0-9_]+$"))]
username: String,
#[zod(email)]
email: String,
#[zod(min(13.0), max(120.0), int)]
age: u32,
#[zod(min_length(1), max_length(10))]
interests: Vec<String>,
bio: Option<String>,
#[zod(nonnegative)]
score: f64,
is_active: bool,
}
let user_data = json!({
"username": "alice_dev",
"email": "alice@example.com",
"age": 28,
"interests": ["rust", "programming"],
"score": 95.5,
"is_active": true
});
match User::validate_and_parse(&user_data) {
Ok(user) => println!("Valid user: {:?}", user),
Err(e) => println!("Invalid: {}", e),
}
let schema = User::schema();
match schema.validate(&user_data) {
Ok(_) => println!("Schema validation passed"),
Err(e) => println!("Schema validation failed: {}", e),
}
let user_from_json = User::from_json(r#"{"username":"test","email":"test@example.com",...}"#)?;
The #[zod(...)] attribute supports the following constraints:
String Validation:
min_length(n) - Minimum string lengthmax_length(n) - Maximum string lengthstarts_with("value") - String starts with a given valueends_with("value") - String ends with a given valueincludes("value") - String includes a given valuelength(n) - Exact string lengthemail - Email format validationurl - URL format validationregex("pattern") - Regular expression pattern matchingNumber Validation:
min(n) - Minimum valuemax(n) - Maximum valueint - Integer only (no decimals)positive - Must be positive (> 0)negative - Must be negative (< 0)nonnegative - Must be non-negative (>= 0)nonpositive - Must be non-positive (<= 0)finite - Must be finite (excludes NaN, Infinity)Array Validation:
min_length(n) - Minimum array lengthmax_length(n) - Maximum array lengthlength(n) - Exact array lengthThe derive macro automatically handles nested structs:
#[derive(Debug, Serialize, Deserialize, ZodSchema)]
struct Address {
#[zod(min_length(5), max_length(200))]
street: String,
#[zod(min_length(2), max_length(50))]
city: String,
#[zod(length(2))]
country_code: String,
}
#[derive(Debug, Serialize, Deserialize, ZodSchema)]
struct UserProfile {
#[zod(min_length(2), max_length(50))]
name: String,
#[zod(email)]
email: String,
address: Option<Address>,
}
The ZodSchema derive macro generates the following methods:
schema() - Returns the validation schemavalidate_and_parse(value) - Validates and deserializes JSON valuefrom_json(json_str) - Validates and parses from JSON stringvalidate_json(json_str) - Validates JSON string (returns Value)use zod_rs::prelude::*;
use zod_rs_util::{ValidationError, ValidateResult};
use serde_json::Value;
struct CustomSchema {
min_words: usize,
}
impl Schema<String> for CustomSchema {
fn validate(&self, value: &Value) -> ValidateResult<String> {
let string_val = value.as_str()
.ok_or_else(|| ValidationError::invalid_type("string", "other"))?
.to_string();
let word_count = string_val.split_whitespace().count();
if word_count < self.min_words {
return Err(ValidationError::custom(
format!("Must contain at least {} words", self.min_words)
).into());
}
Ok(string_val)
}
}
let schema = CustomSchema { min_words: 3 };
assert!(schema.safe_parse(&json!("hello world rust")).is_ok());
assert!(schema.safe_parse(&json!("hello world")).is_err());
use zod_rs::prelude::*;
fn email_schema() -> impl Schema<String> {
string().email()
}
fn password_schema() -> impl Schema<String> {
string().min(8).regex(r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)")
}
fn login_schema() -> impl Schema<Value> {
object()
.field("email", email_schema())
.field("password", password_schema())
}
Run the test suite:
cargo test
Run examples:
# Basic usage
cargo run --example basic_usage
# Struct validation
cargo run --example struct_validation
# Derive macro for schema inference
cargo run --example derive_schema
# Validator crate replacement
cargo run --example validator_replacement
# Axum integration
cargo run --example axum_usage --features axum
This project uses a Cargo workspace with the following crates:
zod-rs - Main validation library with schema typeszod-rs-util - Utility functions, error handling and i18nContributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
cargo testcargo run --example basic_usagecargo fmtcargo clippyThis project is licensed under either of
at your option.
zod-rs provides significant advantages over the traditional validator crate:
| Feature | zod-rs | validator crate |
|---|---|---|
| Schema Definition | Derive macro with attributes | Struct attributes only |
| Runtime Flexibility | Dynamic schema creation | Compile-time only |
| Error Messages | Detailed with full path context | Basic field-level errors |
| JSON Integration | Built-in JSON validation/parsing | Manual serde integration |
| Nested Validation | Automatic nested struct support | Manual implementation |
| Schema Reuse | Composable and reusable schemas | Struct-bound validation |
| Type Safety | Full type inference | Limited type information |
| Performance | Optimized validation pipeline | Direct field validation |
| Extensibility | Custom validators and schemas | Custom validation functions |
| Framework Integration | Built-in web framework support | Manual integration required |
| Internationalization | Built-in Localized error messages | No i18n support |
// Before: using validator crate
use validator::{Validate, ValidationError};
#[derive(Validate)]
struct User {
#[validate(length(min = 3, max = 20))]
username: String,
#[validate(email)]
email: String,
#[validate(range(min = 13, max = 120))]
age: u32,
}
// After: using zod-rs
use zod_rs::prelude::*;
#[derive(ZodSchema)]
struct User {
#[zod(min_length(3), max_length(20))]
username: String,
#[zod(email)]
email: String,
#[zod(min(13.0), max(120.0), int)]
age: u32,
}
// Enhanced capabilities with zod-rs
let user_data = json!({
"username": "alice",
"email": "alice@example.com",
"age": 25
});
// Validate and parse in one step
let user = User::validate_and_parse(&user_data)?;
// Or validate JSON string directly
let user = User::from_json(r#"{"username":"alice",...}"#)?;
// Reuse schema for different purposes
let schema = User::schema();
let is_valid = schema.validate(&user_data).is_ok();
Made with ๐ฆ and โค๏ธ by Maulana Sodiqin