Schema Validation (Validator & Garde)
Enforce rigorous schema invariants, data integrity, and payload validation using validator (20M+ downloads) or garde.
This guide covers the Rust counterpart of Python’s pydantic integration (xstate-statemachine[pydantic]).
🎯 Architecture: Validation Guards
flowchart LR
Event["Incoming Event: SUBMIT"] --> Parse["Deserialize Payload"]
Parse --> Validate{"validator::Validate"}
Validate -->|Valid| Guard["Guard passes -> Microstep transitions"]
Validate -->|ValidationErrors| Reject["Return Validation Receipt / 422 Unprocessable"]
In xstate-statemachine, validation can be applied at two levels:
- Event Payload Validation: Validate incoming request parameters before dispatching into the statechart.
- Context Invariant Validation: Enforce that context properties (e.g. email formats, positive currency amounts, non-empty collections) remain valid after every action assignment.
📦 Dependencies
[dependencies]
validator = { version = "0.19", features = ["derive"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
xstate-statemachine = "0.1"
🛠️ Implementation: Validated Context & Guards
1. Define Validated Structs
#![allow(unused)]
fn main() {
use serde::{Deserialize, Serialize};
use validator::{Validate, ValidationError};
#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
pub struct OrderContext {
#[validate(length(min = 1, message = "Customer ID cannot be empty"))]
pub customer_id: String,
#[validate(email(message = "Invalid notification email"))]
pub email: String,
#[validate(range(min = 100, max = 1_000_000, message = "Amount must be between $1 and $10,000"))]
pub amount_cents: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
pub struct PaymentPayload {
#[validate(length(equal = 3, message = "Currency must be 3-letter ISO code"))]
pub currency: String,
#[validate(credit_card(message = "Invalid card number"))]
pub card_number: String,
}
}
2. Statechart with Validation Guards
#![allow(unused)]
fn main() {
use xstate_statemachine::prelude::*;
pub fn build_validated_logic() -> MachineLogic {
MachineLogic::dynamic()
.guard("isValidPayment", |_ctx, event, _params| {
// Extract event payload and validate against schema
if let Some(payload_val) = &event.payload {
let payload: Result<PaymentPayload, _> = serde_json::from_value(payload_val.clone());
match payload {
Ok(p) => p.validate().is_ok(),
Err(_) => false,
}
} else {
false
}
})
.guard("isValidContext", |ctx, _event, _params| {
// Validate entire machine context
let parsed: Result<OrderContext, _> = serde_json::from_value(ctx.clone());
match parsed {
Ok(c) => c.validate().is_ok(),
Err(_) => false,
}
})
}
}
🚀 Pre-Transition Validation Hook Pattern
You can encapsulate validation into an ergonomic dispatcher function:
#![allow(unused)]
fn main() {
pub fn send_validated_event<T: Validate + for<'de> serde::Deserialize<'de>>(
interp: &mut SyncInterpreter,
event_name: &'static str,
payload: serde_json::Value,
) -> Result<Receipt, Box<dyn std::error::Error>> {
// 1. Validate payload schema first
let typed_payload: T = serde_json::from_value(payload.clone())?;
typed_payload.validate()?;
// 2. Dispatch valid event to statechart
let event = Event::new(event_name).with_payload(payload);
let receipt = interp.send(event)?;
Ok(receipt)
}
}