rydantic 0.8.0

Type-driven validation, JSON Schema, Cap'n Proto, Axum and SQLx models for Rust
Documentation
use rydantic::{json, Context, Model, ValidationError};
use std::sync::{Arc, Mutex};

struct Audit {
    accept_fallback: bool,
    events: Mutex<Vec<&'static str>>,
}
fn reject_first(_: &i64, ctx: &Context) -> Result<(), ValidationError> {
    ctx.require_data::<Audit>()?
        .events
        .lock()
        .unwrap()
        .push("first");
    Err(ValidationError::new("first_rejected", "try the fallback"))
}
fn check_fallback(_: &i64, ctx: &Context) -> Result<(), ValidationError> {
    let audit = ctx.require_data::<Audit>()?;
    let mut events = audit.events.lock().unwrap();
    assert_eq!(&*events, &["first"]);
    events.push("fallback");
    if audit.accept_fallback {
        Ok(())
    } else {
        Err(ValidationError::new(
            "fallback_rejected",
            "no branch accepted",
        ))
    }
}
#[derive(Debug, Model)]
#[model(untagged)]
enum Choice {
    First {
        #[model(validate_with_context = reject_first)]
        value: i64,
    },
    Fallback {
        #[model(validate_with_context = check_fallback)]
        value: i64,
    },
}
#[derive(Debug, Model)]
struct Envelope {
    choices: Vec<Choice>,
}

#[test]
fn failed_untagged_branch_preserves_shared_effects_but_not_errors() {
    for raw in [false, true] {
        let audit = Arc::new(Audit {
            accept_fallback: true,
            events: Mutex::new(vec![]),
        });
        let result = if raw {
            Envelope::parse_json_with_context(r#"{"choices":[{"value":3}]}"#, audit.clone())
        } else {
            Envelope::parse_value_with_context(json!({"choices":[{"value":3}]}), audit.clone())
        };
        let model = result.unwrap();
        assert!(matches!(model.choices[0], Choice::Fallback { value: 3 }));
        assert_eq!(&*audit.events.lock().unwrap(), &["first", "fallback"]);
        assert_eq!(Arc::strong_count(&audit), 1);
    }
}
#[test]
fn failed_untagged_branches_keep_relative_errors_and_release_context() {
    for raw in [false, true] {
        let audit = Arc::new(Audit {
            accept_fallback: false,
            events: Mutex::new(vec![]),
        });
        let result = if raw {
            Envelope::parse_json_with_context(r#"{"choices":[{"value":3}]}"#, audit.clone())
        } else {
            Envelope::parse_value_with_context(json!({"choices":[{"value":3}]}), audit.clone())
        };
        let errors = result.unwrap_err();
        assert_eq!(errors.len(), 1);
        let error = &errors.errors()[0];
        assert_eq!(error.code, "union_no_match");
        assert_eq!(error.pointer(), "/choices/0");
        let branches = error.params["branches"].as_array().unwrap();
        assert_eq!(branches.len(), 2);
        for (branch, code) in branches.iter().zip(["first_rejected", "fallback_rejected"]) {
            let failures = branch["errors"].as_array().unwrap();
            assert_eq!(failures.len(), 1);
            assert_eq!(failures[0]["loc"], json!(["value"]));
            assert_eq!(failures[0]["code"], code);
        }
        assert_eq!(&*audit.events.lock().unwrap(), &["first", "fallback"]);
        assert_eq!(Arc::strong_count(&audit), 1);
    }
}