Skip to main content

Crate scrutiny

Crate scrutiny 

Source
Expand description

§Scrutiny

A powerful, Laravel-inspired validation library for Rust. Uses derive macros and field-level attributes to declaratively validate structs — no runtime string parsing.

§Correct by default

Format rules delegate to dedicated, standards-compliant crates rather than hand-rolled regexes:

RuleStandardCrateFeature
emailRFC 5321email_addressemail
urlWHATWG URLurlurl-parse
uuidRFC 4122uuiduuid-parse
ulidULID speculidulid-parse
date / datetimeISO 8601chronochrono
timezoneIANA tz databasechrono-tztimezone
ip / ipv4 / ipv6RFC 791 / 2460std::net

All enabled by default. Disable default features for a minimal build and opt in to what you need.

§Quick Start

use scrutiny::Validate;
use scrutiny::traits::Validate as _;

#[derive(Validate)]
struct CreateUser {
    #[validate(required, email, bail)]
    email: Option<String>,

    #[validate(required, min = 2, max = 255)]
    name: Option<String>,

    #[validate(required, min = 8, confirmed)]
    password: Option<String>,

    password_confirmation: Option<String>,

    #[validate(nullable, url)]
    website: Option<String>,
}

let user = CreateUser {
    email: Some("test@example.com".into()),
    name: Some("Jane".into()),
    password: Some("secret123".into()),
    password_confirmation: Some("secret123".into()),
    website: None,
};
assert!(user.validate().is_ok());

§Custom Error Messages

Every rule has a sensible default message. Override per-rule with message:

use scrutiny::Validate;
use scrutiny::traits::Validate as _;

#[derive(Validate)]
#[validate(attributes(name = "full name"))]
struct Profile {
    #[validate(required(message = "We need your name!"), min = 2)]
    name: Option<String>,

    #[validate(required, email(message = "That doesn't look right."))]
    email: Option<String>,
}

let p = Profile { name: None, email: Some("bad".into()) };
let err = p.validate().unwrap_err();
assert_eq!(err.messages()["name"][0], "We need your name!");
assert_eq!(err.messages()["email"][0], "That doesn't look right.");

§Nested & Vec Validation

Use nested to recursively validate nested structs and Vec elements. Errors use dot-notation paths: address.city, members.0.email.

use scrutiny::Validate;
use scrutiny::traits::Validate as _;

#[derive(Validate)]
struct Address {
    #[validate(required)]
    city: Option<String>,
}

#[derive(Validate)]
struct Order {
    #[validate(nested)]
    address: Address,
}

let order = Order { address: Address { city: None } };
let err = order.validate().unwrap_err();
assert!(err.messages().contains_key("address.city"));

§Tuple Structs

Newtypes get validation for free — encode your invariants in the type system:

use scrutiny::Validate;
use scrutiny::traits::Validate as _;

#[derive(Validate)]
struct Email(#[validate(email)] String);

#[derive(Validate)]
struct Score(#[validate(min = 0, max = 100)] i32);

assert!(Email("user@example.com".into()).validate().is_ok());
assert!(Score(101).validate().is_err());

§Enums

Validate fields per variant. Unit variants always pass.

use scrutiny::Validate;
use scrutiny::traits::Validate as _;

#[derive(Validate)]
enum ContactMethod {
    Email {
        #[validate(required, email)]
        address: Option<String>,
    },
    Phone {
        #[validate(required, min = 5)]
        number: Option<String>,
    },
    None,
}

let c = ContactMethod::Email { address: Some("bad".into()) };
assert!(c.validate().is_err());

let c = ContactMethod::None;
assert!(c.validate().is_ok());

§Restricting Enum Variants

Use in_list/not_in with strum’s AsRefStr to restrict which variants are accepted:

#[derive(Deserialize, strum::AsRefStr)]
enum UserStatus { Active, Inactive, Banned }

#[derive(Validate, Deserialize)]
struct CreateUser {
    #[validate(in_list("Active", "Inactive"))]
    status: UserStatus,
}

Works on any type implementing AsRef<str>.

§Conditional Validation

use scrutiny::Validate;
use scrutiny::traits::Validate as _;

#[derive(Validate)]
struct Registration {
    #[validate(required, in_list("user", "admin"))]
    role: Option<String>,

    #[validate(required_if(field = "role", value = "admin"))]
    admin_code: Option<String>,
}

// admin_code only required when role is "admin"
let reg = Registration { role: Some("user".into()), admin_code: None };
assert!(reg.validate().is_ok());

§Available Rules

§Presence & Meta

required, filled, nullable, sometimes, bail, prohibited, prohibited_if, prohibited_unless

§Type & Format

string, integer, numeric, boolean, email, url, uuid, ulid, ip, ipv4, ipv6, mac_address, json, ascii, hex_color, timezone

§String

alpha, alpha_num, alpha_dash, uppercase, lowercase, starts_with, ends_with, doesnt_start_with, doesnt_end_with, contains, doesnt_contain, regex, not_regex

§Size, Length & Range

min, max, between, sizetype-aware: compares numeric values for number fields, string length for String, and item count for Vec.

digits, digits_between, decimal, multiple_of

§Comparison

same, different, confirmed, gt, gte, lt, lte, in_list, not_in, in_array, distinct

§Conditional

required_if, required_unless, required_with, required_without, required_with_all, required_without_all, accepted, accepted_if, declined, declined_if

§Date (ISO 8601 strict)

date, datetime, date_equals, before, after, before_or_equal, after_or_equal

§Structural

nested (alias: dive), custom

§Typed Fields & Deserialization Errors

Use actual types like uuid::Uuid or chrono::NaiveDate instead of validating strings manually. Deserialization errors become field-level validation errors automatically.

Axum usersValid<T> handles this out of the box. Just use typed fields.

Everyone else — use deserialize::from_json to get unified errors:

use scrutiny::deserialize::from_json;

// id: uuid::Uuid — if "not-a-uuid" is sent, you get:
// {"id": ["invalid type: expected UUID"]}
match from_json::<CreateUser>(body_bytes) {
    Ok(user) => { /* deserialized AND validated */ }
    Err(errors) => { /* same ValidationErrors type for both */ }
}

§Error Serialization

With the serde feature (default), ValidationErrors serializes to:

{
  "email": ["The email field is required."],
  "name": ["The name field must be at least 2 characters."]
}

Modules§

deserialize
Deserialize-and-validate helpers.
error
Validation error types.
rules
Built-in validation rule functions.
traits
Core traits for the validation system.
value
Type-erased field values for cross-field comparison.

Derive Macros§

Validate