rusdantic 0.1.0

A high-ergonomics data validation and serialization framework for Rust, inspired by Python's Pydantic
Documentation

Rusdantic

Crates.io Documentation CI License MSRV

Rusdantic is a high-ergonomics data validation and serialization framework for Rust, inspired by Python's Pydantic. It bridges the gap between Serde and validation crates like validator / garde into a single, unified derive macro.

Why Rusdantic?

In the Rust ecosystem, validation is fragmented across multiple crates. Rusdantic unifies serialization, deserialization, and validation into one derive macro:

  • One Derive, Three Traits: #[derive(Rusdantic)] generates Serialize, Deserialize, and Validate
  • Validate-on-Deserialize: Invalid structs never exist in memory when using from_json()
  • Path-Aware Errors: Get precise error paths like user.addresses[0].zip_code
  • 7 Built-in Validators: length, range, email, url, pattern, contains, required
  • Custom Validators: Field-level and struct-level cross-field validation
  • Serde Compatible: Works with rename_all, deny_unknown_fields, and other serde attributes
  • JSON Schema: Generate Draft 2020-12 / OpenAPI 3.1 schemas from your types
  • PII Redaction: #[rusdantic(redact)] hides sensitive data in Debug output
  • Sanitizers: trim, lowercase, uppercase, truncate during deserialization
  • Partial Validation: Validate subsets of fields for PATCH endpoints
  • Zero-Cost: Validation logic is monomorphized at compile time

Quick Start

Add rusdantic to your Cargo.toml:

[dependencies]
rusdantic = "0.1.0"

Basic Usage

use rusdantic::prelude::*;

#[derive(Rusdantic, Debug)]
struct User {
    #[rusdantic(length(min = 3, max = 20))]
    username: String,

    #[rusdantic(email)]
    email: String,

    #[rusdantic(range(min = 18))]
    age: u8,
}

fn main() {
    // Deserialize + validate in one step
    let json = r#"{"username": "rust_ace", "email": "user@example.com", "age": 25}"#;
    let user: User = rusdantic::from_json(json).unwrap();
    println!("Valid user: {:?}", user);

    // Invalid data returns all errors at once
    let bad_json = r#"{"username": "ab", "email": "bad", "age": 16}"#;
    match rusdantic::from_json::<User>(bad_json) {
        Ok(_) => unreachable!(),
        Err(e) => println!("{}", e),
        // Output:
        // username: must be at least 3 characters (length_min)
        // email: invalid email format (email)
        // age: must be at least 18 (range_min)
    }
}

Nested Structs with Path-Aware Errors

#[derive(Rusdantic, Debug, Clone)]
struct Address {
    #[rusdantic(length(min = 1))]
    street: String,

    #[rusdantic(pattern(regex = r"^\d{5}$"))]
    zip_code: String,
}

#[derive(Rusdantic, Debug)]
struct UserProfile {
    #[rusdantic(length(min = 1))]
    name: String,

    #[rusdantic(nested)]
    addresses: Vec<Address>,
}

// Errors include full paths: "addresses[1].zip_code: must match pattern..."

Custom Validators

fn validate_not_reserved(value: &String) -> Result<(), ValidationError> {
    if ["admin", "root"].contains(&value.as_str()) {
        Err(ValidationError::new("reserved", "username is reserved"))
    } else {
        Ok(())
    }
}

// Cross-field validation
fn validate_date_range(value: &Event) -> Result<(), ValidationErrors> {
    // Access all fields of the struct for cross-field checks
    // ...
}

#[derive(Rusdantic, Debug, Clone)]
#[rusdantic(custom(function = validate_date_range))]
struct Event {
    #[rusdantic(custom(function = validate_not_reserved))]
    organizer: String,
    start_date: String,
    end_date: String,
}

Sanitizers

#[derive(Rusdantic, Debug)]
struct Registration {
    #[rusdantic(trim, lowercase, email)]
    email: String,  // "  User@EXAMPLE.COM  " → "user@example.com"

    #[rusdantic(trim, length(min = 3))]
    username: String,  // "  ab  " → "ab" → fails length(min=3)

    #[rusdantic(truncate(max = 100))]
    bio: String,
}

PII Redaction

#[derive(Rusdantic)]
struct UserData {
    name: String,

    #[rusdantic(redact)]
    email: String,              // Debug shows: [REDACTED]

    #[rusdantic(redact(with = "***"))]
    ssn: String,                // Debug shows: ***

    #[rusdantic(redact(hash))]
    api_key: String,            // Debug shows: [HASH:a1b2c3d4...]
}

JSON Schema Generation

let schema = User::json_schema();
// Produces:
// {
//   "$schema": "https://json-schema.org/draft/2020-12/schema",
//   "title": "User",
//   "type": "object",
//   "properties": {
//     "username": { "type": "string", "minLength": 3, "maxLength": 20 },
//     "email": { "type": "string", "format": "email" },
//     "age": { "type": "integer" }
//   },
//   "required": ["username", "email", "age"]
// }

Partial Validation (PATCH endpoints)

let user = User { /* ... */ };

// Only validate specific fields
user.validate_partial(&["username", "email"])?;

Feature Comparison

Feature serde + validator serde + garde Rusdantic
Setup 3+ crates 2+ crates 1 crate
Validate on deser No (invalid structs in memory) No Yes
Error paths Manual (serde_path_to_error) Built-in Built-in
Type coercion serde_with per-field No Configurable
Sanitizers No No Built-in
JSON Schema Separate (schemars) No Built-in
PII Redaction No No Built-in
Partial validation No No Built-in

Available Validators

Attribute Description Example
length(min, max) String/collection length #[rusdantic(length(min = 1, max = 255))]
range(min, max) Numeric bounds #[rusdantic(range(min = 0, max = 100))]
email Email format #[rusdantic(email)]
url URL format #[rusdantic(url)]
pattern(regex) Regex match #[rusdantic(pattern(regex = "^[a-z]+$"))]
contains(value) Substring check #[rusdantic(contains(value = "@"))]
required Option must be Some #[rusdantic(required)]
custom(function) Custom validator #[rusdantic(custom(function = my_fn))]
nested Recursive validation #[rusdantic(nested)]

Available Sanitizers

Attribute Description
trim Strip leading/trailing whitespace
lowercase Convert to lowercase
uppercase Convert to uppercase
truncate(max = N) Truncate to N characters

Struct-Level Attributes

Attribute Description
rename_all = "camelCase" Rename all fields (serde-compatible)
deny_unknown_fields Reject unknown JSON keys
custom(function = fn) Cross-field validation

Contributing

We welcome contributions! Please see our CONTRIBUTING.md for details.

License

Licensed under either of:

at your option.


Inspired by the ergonomics of Pydantic, built for the safety of Rust.