Skip to content

Nested Structs

The ZodSchema derive macro automatically handles nested structs. When a field’s type also derives ZodSchema, its schema is used for validation.

use serde::{Deserialize, Serialize};
use serde_json::json;
use zod_rs::prelude::*;
#[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>,
}

When the derive macro encounters a field whose type implements ZodSchema, it calls T::schema() to get the nested validation schema. This happens automatically — no extra attributes are needed.

Use Option<T> for optional nested objects:

#[derive(Debug, Serialize, Deserialize, ZodSchema)]
struct Order {
#[zod(min_length(1))]
id: String,
shipping_address: Address, // required
billing_address: Option<Address>, // optional
}

Vectors of structs work the same way:

#[derive(Debug, Serialize, Deserialize, ZodSchema)]
struct Team {
name: String,
#[zod(min_length(1))]
members: Vec<UserProfile>,
}

Errors in nested structs include the full path:

let data = json!({
"name": "Alice",
"email": "alice@example.com",
"address": {
"street": "1",
"city": "B",
"country_code": "USA"
}
});
match UserProfile::validate_and_parse(&data) {
Err(e) => println!("{}", e),
// Output:
// - address.street: Too small: expected string length >= 5
// - address.country_code: Expected exactly 2 characters
_ => {}
}