Getting started

Quick start

Define a schema, parse JSON, handle errors and serve it over HTTP in a few minutes.

1. Parse a single value

Every schema implements the VldSchema trait. parse() accepts a JSON string, bytes, a file path or a serde_json::Value.

use vld::prelude::*;

let email = vld::string().trim().email();

assert_eq!(email.parse(r#"" ada@example.com ""#).unwrap(), "ada@example.com");
assert!(email.parse(r#""not-an-email""#).is_err());

// From an existing serde_json::Value (borrowed, not cloned):
let value = serde_json::json!("ada@example.com");
assert!(email.parse(&value).is_ok());

2. Describe a struct

vld::schema! declares a struct and the schema of every field. It generates parse(), parse_value(), validate() and is_valid().

use vld::prelude::*;

vld::schema! {
    #[derive(Debug, Clone)]
    pub struct Address {
        pub city: String => vld::string().min(1),
        pub zip: String => vld::string().len(5),
    }
}

vld::schema! {
    #[derive(Debug)]
    pub struct User {
        pub name: String => vld::string().min(2).max(50),
        pub email: String => vld::string().email(),
        pub age: Option<i64> => vld::number().int().gte(18).optional(),
        pub tags: Vec<String> => vld::array(vld::string()).max_len(5).with_default(vec![]),
        pub address: Address => vld::nested(Address::parse_value),
    }
}

let user = User::parse(r#"{
    "name": "Ada",
    "email": "ada@example.com",
    "address": {"city": "London", "zip": "12345"}
}"#).unwrap();

assert_eq!(user.age, None);
assert!(user.tags.is_empty());

Prefer attributes? The same struct with #[derive(Validate)] (feature derive) is shown in Derive.

3. Read the errors

Parsing never stops at the first problem. VldError holds a list of ValidationIssues with a typed code, a message and a path.

let err = User::parse(r#"{"name": "A", "email": "nope", "address": {"city": ""}}"#).unwrap_err();

for issue in &err.issues {
    let path: String = issue.path.iter().map(|p| p.to_string()).collect();
    println!("{path}: [{}] {}", issue.code.key(), issue.message);
}
// .name: [too_small] String must be at least 2 characters
// .email: [invalid_string] Invalid email address
// .address.city: [too_small] String must be at least 1 character
// .address.zip: [missing_field] Required field is missing

VldError implements Display and std::error::Error, so ? works with anyhow and Box<dyn Error>. For forms and APIs use flatten_error, treeify_error or prettify_error.

4. Use it in a handler

Integration crates turn any schema! / derive type into an extractor. With Axum:

Cargo.toml
[dependencies]
vld = "0.5"
vld-axum = "0.5"
axum = "0.8"
tokio = { version = "1", features = ["full"] }
use axum::{routing::post, Router};
use vld_axum::VldJson;

vld::schema! {
    #[derive(Debug)]
    pub struct CreateUser {
        pub name: String => vld::string().min(2).max(50),
        pub email: String => vld::string().email(),
    }
}

async fn create_user(VldJson(body): VldJson<CreateUser>) -> String {
    format!("created {}", body.name)
}

#[tokio::main]
async fn main() {
    let app = Router::new().route("/users", post(create_user));
    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

Invalid bodies get a 422 Unprocessable Entity with every issue; a wrong Content-Type gets 415. The response format is the same for all web crates, see the web overview.

Next steps

  • Schemas: every built-in type and check.
  • Structs: schema!, derive, renames, lenient parsing.
  • Errors: issue codes, custom messages, translations.
  • OpenAPI: JSON Schema output and diffs.