This guide explains how to use the automatic validation and filtering system built into the framework. The system automatically applies validators and filters before your handlers receive the data, ensuring that data is always clean and valid.
The system consists of:
impl_data_entity_validated! - Declarative definition in model.rsValidated<T> - Automatic extraction in handlers// entities/invoice/model.rs
use this::prelude::*;
impl_data_entity_validated!(
Invoice,
"invoice",
["name", "number"],
{
number: String,
amount: f64,
due_date: Option<String>,
paid_at: Option<String>,
},
// Validation rules by operation
validate: {
create: {
number: [required string_length(3, 50)],
amount: [required positive max_value(1_000_000.0)],
status: [required in_list("draft", "sent", "paid", "cancelled")],
due_date: [optional date_format("%Y-%m-%d")],
},
update: {
amount: [optional positive max_value(1_000_000.0)],
status: [optional in_list("draft", "sent", "paid", "cancelled")],
},
},
// Filters by operation
filters: {
create: {
number: [trim uppercase],
status: [trim lowercase],
amount: [round_decimals(2)],
},
update: {
status: [trim lowercase],
amount: [round_decimals(2)],
},
}
);
// entities/invoice/handlers.rs
use this::prelude::Validated;
pub async fn create_invoice(
State(state): State<InvoiceAppState>,
validated: Validated<Invoice>, // ← Automatic validation!
) -> Result<Json<Invoice>, StatusCode> {
// Data is already filtered and validated!
let payload = &*validated;
let invoice = Invoice::new(
payload["number"].as_str().unwrap().to_string(),
payload["status"].as_str().unwrap().to_string(),
payload["number"].as_str().unwrap().to_string(),
payload["amount"].as_f64().unwrap(),
payload["due_date"].as_str().map(String::from),
payload["paid_at"].as_str().map(String::from),
);
state.store.add(invoice.clone());
Ok(Json(invoice))
}
requiredChecks that the field is not null.
number: [required]
optionalMarks the field as optional (always valid).
due_date: [optional]
positiveChecks that the number is positive (> 0).
amount: [positive]
string_length(min, max)Checks the length of a string.
number: [string_length(3, 50)]
max_value(max)Checks that the number does not exceed a maximum value.
amount: [max_value(1_000_000.0)]
in_list("val1", "val2", ...)Checks that the value is in an allowed list.
status: [in_list("draft", "sent", "paid", "cancelled")]
date_format(format)Checks that a date matches the specified format.
due_date: [date_format("%Y-%m-%d")]
trimRemoves spaces at the beginning and end of a string.
number: [trim]
uppercaseConverts a string to uppercase.
number: [uppercase]
lowercaseConverts a string to lowercase.
status: [lowercase]
round_decimals(decimals)Rounds a number to the specified number of decimals.
amount: [round_decimals(2)]
impl_data_entity_validated!(
User,
"user",
["name", "email"],
{
email: String,
age: u32,
},
validate: {
create: {
name: [required string_length(2, 100)],
email: [required],
age: [required positive],
},
},
filters: {
create: {
name: [trim],
email: [trim lowercase],
},
}
);
impl_data_entity_validated!(
Product,
"product",
["name", "sku"],
{
sku: String,
price: f64,
category: String,
},
validate: {
create: {
sku: [required string_length(5, 20)],
price: [required positive max_value(999999.99)],
category: [required in_list("electronics", "clothing", "food")],
},
},
filters: {
create: {
sku: [trim uppercase],
price: [round_decimals(2)],
category: [trim lowercase],
},
}
);
impl_data_entity_validated!(
Order,
"order",
["number", "status"],
{
number: String,
amount: f64,
notes: Option<String>,
},
validate: {
create: {
number: [required string_length(5, 30)],
amount: [required positive],
},
update: {
amount: [optional positive],
notes: [optional string_length(0, 500)],
},
},
filters: {
create: {
number: [trim uppercase],
amount: [round_decimals(2)],
},
update: {
amount: [round_decimals(2)],
notes: [trim],
},
}
);
When a request arrives:
If validation fails, an HTTP 422 response is returned with details:
{
"error": "Validation failed",
"errors": [
"Field 'amount' must be positive (value: -100)",
"'status' must be one of: [\"draft\", \"sent\", \"paid\"] (current value: invalid)"
]
}
// src/core/validation/validators.rs
pub fn email_format() -> impl Fn(&str, &Value) -> Result<(), String> + Send + Sync + Clone {
|field: &str, value: &Value| {
if let Some(s) = value.as_str() {
if s.contains('@') && s.contains('.') {
Ok(())
} else {
Err(format!("'{}' must be a valid email address", field))
}
} else {
Ok(())
}
}
}
Then add it to the macro helper:
// src/entities/macros.rs - add_validators_for_field!
($config:expr, $field:expr, email_format $( $rest:tt )*) => {
$config.add_validator($field, $crate::core::validation::validators::email_format());
$crate::add_validators_for_field!($config, $field, $( $rest )*);
};
// src/core/validation/filters.rs
pub fn slugify() -> impl Fn(&str, Value) -> Result<Value> + Send + Sync + Clone {
|_: &str, value: Value| {
if let Some(s) = value.as_str() {
let slug = s.to_lowercase()
.chars()
.map(|c| if c.is_alphanumeric() || c == '-' { c } else { '-' })
.collect::<String>();
Ok(Value::String(slug))
} else {
Ok(value)
}
}
}
// In main.rs
tracing_subscriber::fmt()
.with_max_level(tracing::Level::DEBUG)
.init();
# Test with invalid data
curl -X POST http://127.0.0.1:3000/invoices \
-H "Content-Type: application/json" \
-d '{"number": " inv-test ", "status": " DRAFT ", "amount": 1234.567}'
# Expected result:
# - number: "INV-TEST" (trimmed and uppercased)
# - status: "draft" (trimmed and lowercased)
# - amount: 1234.57 (rounded to 2 decimals)
The automatic validation and filtering system allows you to:
model.rsThe system is 100% integrated with the framework and follows its declarative philosophy!