rydantic 0.8.0

Type-driven validation, JSON Schema, Cap'n Proto, Axum and SQLx models for Rust
Documentation
use rydantic::{json, Model, ValidationError, ValidationErrors, Value};
thread_local! { static TRACE: std::cell::RefCell<Vec<&'static str>> = const { std::cell::RefCell::new(Vec::new()) }; }
fn log(s: &'static str) {
    TRACE.with(|t| t.borrow_mut().push(s));
}
fn trace() -> Vec<&'static str> {
    TRACE.with(|t| std::mem::take(&mut *t.borrow_mut()))
}
fn raw(v: Value) -> Result<Value, ValidationError> {
    log("before1");
    Ok(json!(v.as_i64().unwrap() + 1))
}
fn value(v: Value) -> Result<Value, ValidationError> {
    log("before2");
    Ok(json!(v.as_i64().unwrap() * 2))
}
fn child(_: &i32) -> Result<(), ValidationError> {
    log("check1");
    Ok(())
}
fn start(_: &i32) -> Result<(), ValidationError> {
    log("check2");
    Ok(())
}
fn first(_: &Ordered) -> Result<(), ValidationErrors> {
    log("model1");
    Ok(())
}
fn second(_: &Ordered) -> Result<(), ValidationErrors> {
    log("model2");
    Ok(())
}
#[derive(Debug, Model)]
#[model(validate=[first,second])]
struct Ordered {
    #[model(before=[raw,value],validate=[child,start],default=2)]
    x: i32,
}
#[test]
fn ordered_transforms_checks_and_defaults() {
    for input in [json!({"x":2}), json!({})] {
        let m = Ordered::parse_value(input).unwrap();
        assert_eq!(m.x, 6);
        assert_eq!(
            trace(),
            ["before1", "before2", "check1", "check2", "model1", "model2"]
        );
        m.validate().unwrap();
        assert_eq!(trace(), ["check1", "check2", "model1", "model2"]);
    }
}
fn fail1(_: &i32) -> Result<(), ValidationError> {
    log("fail1");
    Err(ValidationError::new("one", "one"))
}
fn fail2(_: &i32) -> Result<(), ValidationError> {
    log("fail2");
    Err(ValidationError::new("two", "two"))
}
#[derive(Debug, Model)]
struct Fail {
    #[model(validate=[fail1,fail2])]
    a: i32,
    #[model(validate=[fail2,fail1])]
    b: i32,
}
#[test]
fn field_errors_accumulate_in_callback_then_field_order() {
    let e = Fail::parse_value(json!({"a":1,"b":2})).unwrap_err();
    assert_eq!(
        e.errors()
            .iter()
            .map(|e| e.code.as_str())
            .collect::<Vec<_>>(),
        ["one", "two", "two", "one"]
    );
    assert_eq!(trace(), ["fail1", "fail2", "fail2", "fail1"]);
    assert_eq!(e.errors()[0].loc, e.errors()[1].loc);
    assert_ne!(e.errors()[1].loc, e.errors()[2].loc);
}
fn stop(_: Value) -> Result<Value, ValidationError> {
    log("stop");
    Err(ValidationError::new("stop", "stop"))
}
#[derive(Debug, Model)]
struct Stops {
    #[model(before=[stop,raw])]
    a: i32,
    #[model(validate=[child,start])]
    b: i32,
}
#[test]
fn before_fails_fast_but_sibling_checks_continue() {
    assert!(Stops::parse_value(json!({"a":1,"b":2})).is_err());
    assert_eq!(trace(), ["stop", "check1", "check2"]);
}
async fn async1(_: &i32) -> Result<(), ValidationError> {
    log("async1");
    Err(ValidationError::new("async1", "one"))
}
async fn async2(_: &i32) -> Result<(), ValidationError> {
    log("async2");
    Err(ValidationError::new("async2", "two"))
}
#[derive(Debug, Model)]
struct Async {
    #[model(async_validate=[async1,async2])]
    a: i32,
}
fn drive<F: std::future::Future>(f: F) -> F::Output {
    let mut f = std::pin::pin!(f);
    let mut c = std::task::Context::from_waker(std::task::Waker::noop());
    match f.as_mut().poll(&mut c) {
        std::task::Poll::Ready(v) => v,
        _ => panic!("unexpected pending"),
    }
}
#[test]
fn async_checks_accumulate_sequentially() {
    let e = drive(Async::parse_value_async(json!({"a":1}))).unwrap_err();
    assert_eq!(trace(), ["async1", "async2"]);
    assert_eq!(e.len(), 2);
}
fn model_fail1(_: &ModelFail) -> Result<(), ValidationErrors> {
    log("model1");
    Err(ValidationErrors(vec![ValidationError::new(
        "model1", "one",
    )]))
}
fn model_fail2(_: &ModelFail) -> Result<(), ValidationErrors> {
    log("model2");
    Err(ValidationErrors(vec![ValidationError::new(
        "model2", "two",
    )]))
}
#[derive(Debug, Model)]
#[model(validate=[model_fail1,model_fail2])]
struct ModelFail {
    a: i32,
}
#[test]
fn model_checks_accumulate_after_fields_succeed() {
    let e = ModelFail::parse_value(json!({"a":1})).unwrap_err();
    assert_eq!(e.len(), 2);
    assert_eq!(trace(), ["model1", "model2"]);
    assert!(ModelFail::parse_value(json!({"a":"bad"})).is_err());
    assert!(trace().is_empty());
}
async fn model_async1(_: &AsyncModel) -> Result<(), ValidationErrors> {
    log("model_async1");
    Err(ValidationErrors(vec![ValidationError::new(
        "model_async1",
        "one",
    )]))
}
async fn model_async2(_: &AsyncModel) -> Result<(), ValidationErrors> {
    log("model_async2");
    Err(ValidationErrors(vec![ValidationError::new(
        "model_async2",
        "two",
    )]))
}
#[derive(Debug, Model)]
#[model(async_validate=[model_async1,model_async2])]
struct AsyncModel {
    a: i32,
}
#[test]
fn model_async_checks_accumulate() {
    let e = drive(AsyncModel::parse_value_async(json!({"a":1}))).unwrap_err();
    assert_eq!(e.len(), 2);
    assert_eq!(trace(), ["model_async1", "model_async2"]);
}
#[derive(Debug, Model)]
struct Gate {
    #[model(ge=5,validate=[fail1,fail2])]
    a: i32,
}
#[test]
fn list_preserves_parse_vs_revalidate_gate() {
    assert_eq!(Gate::parse_value(json!({"a":1})).unwrap_err().len(), 1);
    assert!(trace().is_empty());
    assert_eq!(Gate { a: 1 }.validate().unwrap_err().len(), 3);
    assert_eq!(trace(), ["fail1", "fail2"]);
}
#[test]
fn input_schema_retains_all_before_annotations() {
    let schema = Ordered::json_schema(rydantic::SchemaMode::Input);
    let text = schema.to_string();
    assert!(text.contains("before:raw"));
    assert!(text.contains("before:value"));
}