{"id":47729203,"url":"https://github.com/georgeboot/scrutiny","last_synced_at":"2026-04-06T00:01:13.709Z","repository":{"id":348022997,"uuid":"1196170066","full_name":"georgeboot/scrutiny","owner":"georgeboot","description":"A powerful, standards-compliant validation library for Rust with 50+ rules, conditional logic, and first-class axum integration.","archived":false,"fork":false,"pushed_at":"2026-04-01T06:25:00.000Z","size":77,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-04-03T06:36:51.759Z","etag":null,"topics":["axum","rust","validate","validation"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/georgeboot.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-30T12:46:03.000Z","updated_at":"2026-04-01T06:22:30.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/georgeboot/scrutiny","commit_stats":null,"previous_names":["georgeboot/scrutiny"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/georgeboot/scrutiny","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/georgeboot%2Fscrutiny","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/georgeboot%2Fscrutiny/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/georgeboot%2Fscrutiny/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/georgeboot%2Fscrutiny/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/georgeboot","download_url":"https://codeload.github.com/georgeboot/scrutiny/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/georgeboot%2Fscrutiny/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31379452,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-03T21:40:47.592Z","status":"ssl_error","status_checked_at":"2026-04-03T21:40:05.436Z","response_time":107,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["axum","rust","validate","validation"],"created_at":"2026-04-02T21:14:49.431Z","updated_at":"2026-04-03T22:01:16.540Z","avatar_url":"https://github.com/georgeboot.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# scrutiny\n\n[![Crates.io](https://img.shields.io/crates/v/scrutiny)](https://crates.io/crates/scrutiny)\n[![docs.rs](https://docs.rs/scrutiny/badge.svg)](https://docs.rs/scrutiny)\n[![CI](https://github.com/georgeboot/scrutiny/actions/workflows/ci.yaml/badge.svg)](https://github.com/georgeboot/scrutiny/actions/workflows/ci.yaml)\n[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n\nA powerful, Laravel-inspired validation library for Rust. Brings Laravel's validation DX to the Rust ecosystem using derive macros and the type system — no runtime string parsing.\n\n**Correct by default.** Format rules delegate to dedicated, standards-compliant parsing crates — not hand-rolled regexes. Email is validated per RFC 5321, URLs per the WHATWG URL Standard, UUIDs per RFC 4122, dates per ISO 8601, and IP addresses via Rust's stdlib. Where a standard exists, we follow it.\n\n## Why?\n\nThe existing Rust validation crates (`validator`, `garde`) are limited: few rules, no conditional validation, no bail, no per-rule custom messages, no framework-aware error responses. They also tend to use simplistic regexes for format validation rather than proper parsers, leading to false positives and negatives.\n\nThis library provides 50+ validation rules, conditional logic, nested validation, first-class axum integration, and standards-compliant format validation out of the box.\n\n### Standards used\n\n| Rule | Standard | Crate |\n|------|----------|-------|\n| `email` | RFC 5321 | [`email_address`](https://crates.io/crates/email_address) |\n| `url` | WHATWG URL | [`url`](https://crates.io/crates/url) |\n| `uuid` | RFC 4122 | [`uuid`](https://crates.io/crates/uuid) |\n| `ulid` | [ULID spec](https://github.com/ulid/spec) | [`ulid`](https://crates.io/crates/ulid) |\n| `date` / `datetime` | ISO 8601 | [`chrono`](https://crates.io/crates/chrono) |\n| `timezone` | IANA tz database | [`chrono-tz`](https://crates.io/crates/chrono-tz) |\n| `ip` / `ipv4` / `ipv6` | RFC 791 / 2460 | `std::net` |\n| `mac_address` | IEEE 802 | trivial format check |\n\nEach is behind a feature flag (all on by default). Disable default features for a minimal build and opt in to what you need.\n\n## Getting Started\n\nAdd to your `Cargo.toml`:\n\n```toml\n[dependencies]\nscrutiny = { path = \"scrutiny\" }\n\n# For axum integration:\nscrutiny-axum = { path = \"scrutiny-axum\" }\n```\n\n### Basic Usage\n\n```rust\nuse scrutiny::Validate;\nuse scrutiny::traits::Validate as _;\n\n#[derive(Validate)]\nstruct CreateUser {\n    #[validate(required, email, bail)]\n    email: Option\u003cString\u003e,\n\n    #[validate(required, min = 2, max = 255)]\n    name: Option\u003cString\u003e,\n\n    #[validate(required, min = 8, confirmed)]\n    password: Option\u003cString\u003e,\n\n    password_confirmation: Option\u003cString\u003e,\n\n    #[validate(nullable, url)]\n    website: Option\u003cString\u003e,\n\n    #[validate(required, in_list(\"user\", \"admin\", \"editor\"))]\n    role: Option\u003cString\u003e,\n}\n\nlet user = CreateUser {\n    email: Some(\"test@example.com\".into()),\n    name: Some(\"Jane Doe\".into()),\n    password: Some(\"secretpassword\".into()),\n    password_confirmation: Some(\"secretpassword\".into()),\n    website: None,\n    role: Some(\"admin\".into()),\n};\n\nassert!(user.validate().is_ok());\n```\n\n### Custom Error Messages\n\nEvery rule has a sensible default message with field name interpolation. Override any rule's message inline:\n\n```rust\n#[derive(Validate)]\n#[validate(attributes(name = \"full name\"))]\nstruct Profile {\n    #[validate(\n        required(message = \"We need your name!\"),\n        min(value = 2, message = \"Name must be at least :min characters.\"),\n    )]\n    name: Option\u003cString\u003e,\n\n    #[validate(\n        required,\n        email(message = \"That doesn't look like a valid email.\"),\n    )]\n    email: Option\u003cString\u003e,\n}\n```\n\nDefault messages use `:attribute` (friendly field name), `:min`, `:max`, etc. The `attributes()` macro maps field names to display names.\n\n### Type-Aware Rules\n\n`min`, `max`, `between`, and `size` automatically detect the field type and do the right thing:\n\n```rust\n#[derive(Validate)]\nstruct Query {\n    #[validate(min = 1, max = 10000)]   // numeric: compares value\n    per_page: f64,\n\n    #[validate(min = 2, max = 255)]     // string: compares length\n    search: String,\n\n    #[validate(min = 1, max = 10)]      // vec: compares item count\n    tags: Vec\u003cString\u003e,\n\n    #[validate(size = 4)]               // vec: exactly 4 items\n    bounding_box: Vec\u003cf64\u003e,\n\n    #[validate(between(min = 0, max = 100))]  // numeric: value in range\n    score: i32,\n}\n```\n\n### Tuple Structs\n\nNewtypes get validation for free — encode your invariants in the type system:\n\n```rust\n#[derive(Validate)]\nstruct Email(#[validate(email)] String);\n\n#[derive(Validate)]\nstruct Score(#[validate(min = 0, max = 100)] i32);\n```\n\nUse them in other structs with `#[validate(nested)]`:\n\n```rust\n#[derive(Validate)]\nstruct UserProfile {\n    #[validate(required)]\n    name: Option\u003cString\u003e,\n    #[validate(nested)]\n    email: Email,\n}\n```\n\n### Enums\n\nValidate fields per variant. Unit variants always pass.\n\n```rust\n#[derive(Validate)]\nenum ContactMethod {\n    Email {\n        #[validate(required, email)]\n        address: Option\u003cString\u003e,\n    },\n    Phone {\n        #[validate(required, min = 5)]\n        number: Option\u003cString\u003e,\n    },\n    None,\n}\n```\n\n**Restricting allowed variants** — use `in_list`/`not_in` with [strum](https://crates.io/crates/strum)'s `AsRefStr`:\n\n```rust\n#[derive(Deserialize, strum::AsRefStr)]\nenum UserStatus { Active, Inactive, Banned, Suspended }\n\n#[derive(Validate, Deserialize)]\nstruct CreateUser {\n    #[validate(in_list(\"Active\", \"Inactive\"))]  // rejects Banned, Suspended\n    status: UserStatus,\n}\n\n#[derive(Validate, Deserialize)]\nstruct AdminUpdate {\n    #[validate(not_in(\"Banned\"))]  // only rejects Banned\n    status: UserStatus,\n}\n```\n\nThis works because `in_list`/`not_in` operate on any type implementing `AsRef\u003cstr\u003e`.\n\nTuple variants work too:\n\n```rust\n#[derive(Validate)]\nenum Wrapper {\n    Text(#[validate(required, min = 1)] Option\u003cString\u003e),\n    Number(#[validate(min = 0, max = 999)] i32),\n    Empty,\n}\n```\n\n### Conditional Validation\n\n```rust\n#[derive(Validate)]\nstruct Registration {\n    #[validate(required, in_list(\"user\", \"admin\"))]\n    role: Option\u003cString\u003e,\n\n    // Only required when role is \"admin\"\n    #[validate(required_if(field = \"role\", value = \"admin\", message = \"Admins need a code.\"))]\n    admin_code: Option\u003cString\u003e,\n\n    // Prohibited for basic users\n    #[validate(prohibited_if(field = \"role\", value = \"user\"))]\n    admin_feature: Option\u003cString\u003e,\n}\n```\n\n### Nested \u0026 Array Validation\n\nUse `nested` to recursively validate nested structs and `Vec` elements. Errors use dot-notation paths.\n\n```rust\n#[derive(Validate)]\nstruct Address {\n    #[validate(required, max = 255)]\n    line1: Option\u003cString\u003e,\n    #[validate(required)]\n    city: Option\u003cString\u003e,\n    #[validate(required, regex(pattern = r\"^\\d{5}(-\\d{4})?$\", message = \"Invalid ZIP.\"))]\n    zip: Option\u003cString\u003e,\n}\n\n#[derive(Validate)]\nstruct Team {\n    #[validate(required)]\n    name: Option\u003cString\u003e,\n    #[validate(nested)]\n    members: Vec\u003cMember\u003e,\n    #[validate(nested)]\n    address: Address,\n}\n\n// Errors: \"address.city\", \"members.0.email\", \"members.2.name\"\n```\n\n### Typed Fields\n\nUse actual types instead of validating strings — deserialization errors become field-level validation errors automatically:\n\n```rust\n#[derive(Validate, Deserialize)]\nstruct CreateUser {\n    #[validate(required, min = 2)]\n    name: Option\u003cString\u003e,\n    id: uuid::Uuid,              // no #[validate(uuid)] needed\n    created: chrono::NaiveDate,  // no #[validate(date)] needed\n}\n```\n\nIf someone sends `{\"name\": null, \"id\": \"not-a-uuid\", \"created\": \"bad\"}`:\n\n```json\n{\n  \"errors\": {\n    \"name\": [\"The name field is required.\"],\n    \"id\": [\"invalid type: string \\\"not-a-uuid\\\", expected UUID\"],\n    \"created\": [\"premature end of input\"]\n  }\n}\n```\n\n**Axum users**: `Valid\u003cT\u003e` handles this out of the box.\n\n**Everyone else**: use `scrutiny::deserialize::from_json` to get the same unified errors:\n\n```rust\nuse scrutiny::deserialize::from_json;\n\nmatch from_json::\u003cCreateUser\u003e(body_bytes) {\n    Ok(user) =\u003e { /* deserialized AND validated */ }\n    Err(errors) =\u003e { /* same ValidationErrors for both deser and validation */ }\n}\n```\n\n### Axum Integration\n\nDrop-in replacement for `axum::Json\u003cT\u003e` — and for `axum_extra::extract::WithRejection`. You don't need `axum-extra` for error customization; our extractors handle deserialization, validation, and error responses in one step.\n\n```rust\n// Before (axum + axum-extra):\nuse axum_extra::extract::WithRejection;\nasync fn handler(\n    WithRejection(Json(body), _): WithRejection\u003cJson\u003cCreateUser\u003e, AppError\u003e,\n) -\u003e Result\u003cimpl IntoResponse\u003e { ... }\n\n// After (scrutiny-axum):\nuse scrutiny_axum::Valid;\nasync fn handler(Valid(body): Valid\u003cCreateUser\u003e) -\u003e impl IntoResponse { ... }\n```\n\nValidates before your handler runs:\n\n```rust\nuse scrutiny_axum::Valid;\n\nasync fn create_user(Valid(user): Valid\u003cCreateUser\u003e) -\u003e impl IntoResponse {\n    // `user` is already validated.\n    // Invalid requests get a 422 JSON response automatically.\n}\n```\n\n**Custom error responses** via trait:\n\n```rust\nuse scrutiny_axum::{ValidWith, ValidationErrorResponse};\n\nstruct MyApiError;\n\nimpl ValidationErrorResponse for MyApiError {\n    fn from_validation_errors(errors: ValidationErrors) -\u003e Response {\n        let body = json!({\n            \"success\": false,\n            \"code\": \"VALIDATION_FAILED\",\n            \"details\": errors.messages(),\n        });\n        (StatusCode::BAD_REQUEST, Json(body)).into_response()\n    }\n\n    fn from_deserialization_error(error: String) -\u003e Response {\n        // ...\n    }\n}\n\nasync fn handler(result: ValidWith\u003cCreateUser, MyApiError\u003e) -\u003e impl IntoResponse {\n    let user = result.into_inner();\n    // ...\n}\n```\n\nAlso available: `ValidForm\u003cT\u003e` and `ValidQuery\u003cT\u003e` for form-encoded and query parameter validation.\n\n## Available Rules (50+)\n\n### Presence \u0026 Meta\n| Rule | Attribute | Description |\n|------|-----------|-------------|\n| required | `required` | Field must be present and non-empty |\n| filled | `filled` | If present, must not be empty |\n| nullable | `nullable` | Skip rules if None |\n| sometimes | `sometimes` | Skip rules if field absent |\n| bail | `bail` | Stop on first error for this field |\n| prohibited | `prohibited` | Field must NOT be present |\n| prohibited_if | `prohibited_if(field, value)` | Prohibited when condition met |\n| prohibited_unless | `prohibited_unless(field, value)` | Prohibited unless condition met |\n\n### Type \u0026 Format\n| Rule | Attribute | Description |\n|------|-----------|-------------|\n| string | `string` | Must be a string (compile-time assertion) |\n| integer | `integer` | Must be a valid integer |\n| numeric | `numeric` | Must be a valid number |\n| boolean | `boolean` | Must be true/false/1/0 |\n| email | `email` | Valid email (HTML5 spec) |\n| url | `url` | Valid URL |\n| uuid | `uuid` | Valid UUID (8-4-4-4-12 hex) |\n| ulid | `ulid` | Valid ULID (26 char Crockford base32) |\n| ip | `ip` | Valid IP address |\n| ipv4 | `ipv4` | Valid IPv4 address |\n| ipv6 | `ipv6` | Valid IPv6 address |\n| mac_address | `mac_address` | Valid MAC address |\n| json | `json` | Valid JSON string |\n| ascii | `ascii` | Only ASCII characters |\n| hex_color | `hex_color` | Valid hex color (#RGB, #RRGGBB, #RRGGBBAA) |\n| timezone | `timezone` | Valid timezone (IANA format) |\n\n### String\n| Rule | Attribute | Description |\n|------|-----------|-------------|\n| alpha | `alpha` | Only alphabetic characters |\n| alpha_num | `alpha_num` | Only alphanumeric |\n| alpha_dash | `alpha_dash` | Alphanumeric + dashes + underscores |\n| uppercase | `uppercase` | Must be entirely uppercase |\n| lowercase | `lowercase` | Must be entirely lowercase |\n| starts_with | `starts_with = \"X\"` | Must start with prefix |\n| ends_with | `ends_with = \"X\"` | Must end with suffix |\n| doesnt_start_with | `doesnt_start_with = \"X\"` | Must NOT start with prefix |\n| doesnt_end_with | `doesnt_end_with = \"X\"` | Must NOT end with suffix |\n| contains | `contains = \"X\"` | Must contain substring |\n| doesnt_contain | `doesnt_contain = \"X\"` | Must NOT contain substring |\n| regex | `regex = \"pattern\"` | Must match regex |\n| not_regex | `not_regex = \"pattern\"` | Must NOT match regex |\n\n### Size \u0026 Length\n| Rule | Attribute | Description |\n|------|-----------|-------------|\n| min | `min = N` | Type-aware: numeric value, string length, or Vec item count |\n| max | `max = N` | Type-aware: numeric value, string length, or Vec item count |\n| between | `between(min, max)` | Type-aware: value/length/count between min and max |\n| size | `size = N` | Type-aware: exact value, length, or count |\n| digits | `digits = N` | Exact digit count |\n| digits_between | `digits_between(min, max)` | Digit count between min and max |\n| decimal | `decimal = N` or `decimal(min, max)` | Exact or range of decimal places |\n| multiple_of | `multiple_of = \"N\"` | Must be a multiple of N |\n\n### Comparison\n| Rule | Attribute | Description |\n|------|-----------|-------------|\n| same | `same = \"field\"` | Must equal another field |\n| different | `different = \"field\"` | Must differ from another field |\n| confirmed | `confirmed` | Must match `{field}_confirmation` |\n| gt | `gt = \"field\"` | Greater than another field |\n| gte | `gte = \"field\"` | Greater than or equal |\n| lt | `lt = \"field\"` | Less than another field |\n| lte | `lte = \"field\"` | Less than or equal |\n| in_list | `in_list(\"a\", \"b\", \"c\")` | Must be one of the values |\n| not_in | `not_in(\"a\", \"b\")` | Must NOT be one of the values |\n| in_array | `in_array = \"field\"` | Must exist in another field's array |\n| distinct | `distinct` | Array items must be unique |\n\n### Conditional\n| Rule | Attribute | Description |\n|------|-----------|-------------|\n| required_if | `required_if(field, value)` | Required when field equals value |\n| required_unless | `required_unless(field, value)` | Required unless field equals value |\n| required_with | `required_with = \"field\"` | Required when field is present |\n| required_without | `required_without = \"field\"` | Required when field is absent |\n| required_with_all | `required_with_all(\"a\", \"b\")` | Required when ALL fields present |\n| required_without_all | `required_without_all(\"a\", \"b\")` | Required when ALL fields absent |\n| accepted | `accepted` | Must be yes/on/1/true |\n| accepted_if | `accepted_if(field, value)` | Must be accepted when condition met |\n| declined | `declined` | Must be no/off/0/false |\n| declined_if | `declined_if(field, value)` | Must be declined when condition met |\n\n### Date (ISO 8601 strict)\n| Rule | Attribute | Description |\n|------|-----------|-------------|\n| date | `date` | Valid ISO 8601 date (YYYY-MM-DD) |\n| datetime | `datetime` | Valid ISO 8601 datetime |\n| date_equals | `date_equals = \"YYYY-MM-DD\"` | Must equal the date |\n| before | `before = \"YYYY-MM-DD\"` | Must be before the date |\n| after | `after = \"YYYY-MM-DD\"` | Must be after the date |\n| before_or_equal | `before_or_equal = \"YYYY-MM-DD\"` | Before or equal |\n| after_or_equal | `after_or_equal = \"YYYY-MM-DD\"` | After or equal |\n\n### Structural\n| Rule | Attribute | Description |\n|------|-----------|-------------|\n| nested | `nested` | Recursively validate nested struct/Vec (alias: `dive`) |\n| custom | `custom = fn_name` | Custom validation function |\n\n## Architecture\n\n```\nscrutiny/          Core: traits, errors, rule functions\nscrutiny-derive/   Proc macro: #[derive(Validate)]\nscrutiny-axum/     Axum extractors + error response customization\n```\n\nThe core is framework-agnostic. `scrutiny-axum` adds axum extractors behind a separate crate. The error system uses `ValidationErrors` with dot-notation field paths and is serde-serializable.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgeorgeboot%2Fscrutiny","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgeorgeboot%2Fscrutiny","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgeorgeboot%2Fscrutiny/lists"}