Quick start
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:
[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.