axum-valid

repository·main·Indexed 41 days ago

https://github.com/gengteng/axum-valid

Validation extractors for the Axum web framework, providing integration with the validator, garde, and validify crates. It allows developers to wrap standard Axum extractors (such as Json, Query, or Form) to automatically perform data validation and return appropriate HTTP responses, such as 400 Bad Request or 422 Unprocessable Entity, when validation fails.

Tokens
9.7K
Snippets
22
Records
27
Agent score
49%

What's inside axum-valid

  1. Overview of axum-valid

    main

    axum-valid is a library providing data validation extractors for the Axum web framework. It integrates three popular Rust validation crates to offer convenient validation and data handling within Axum applications:

    1. validator: Enabled by default.
    2. garde: Requires the garde feature.
    3. validify: Requires the validify feature.

    These extractors allow you to wrap standard Axum extractors (like Json, Query, or Form) to automatically perform validation and return appropriate HTTP responses (e.g., 400 Bad Request) when validation fails.

  2. Use the `Garde<E>` extractor with `garde`

    main

    The Garde<E> extractor is used for validation powered by the garde crate.

    Installation

    cargo add garde --features derive
    cargo add axum --features macros
    cargo add axum-valid --features garde,basic --no-default-features

    Usage

    Wrap an Axum extractor with Garde<E>.

    Important Note on State: If you are using Garde and also using Axum State, you must implement FromRef<StateType> for () (unit type) for your state struct, even if the state is unrelated to Garde. A common pattern is to add an () field to your state and derive FromRef for it.

    Example

    use axum::extract::{Query, Json};
    use axum_valid::Garde;
    use garde::Validate;
    use serde::Deserialize;
    
    #[derive(Debug, Validate, Deserialize)]
    pub struct Paginator {
        #[garde(range(min = 1, max = 50))]
        pub page_size: usize,
        #[garde(range(min = 1))]
        pub page_no: usize,
    }
    
    pub async fn paginator_from_query(Garde(Query(paginator)): Garde<Query<Paginator>>) {
        // paginator is validated
    }
    use axum::extract::{Query, Json};
    use axum_valid::Garde;
    use garde::Validate;
    use serde::Deserialize;
    
    #[derive(Debug, Validate, Deserialize)]
    pub struct Paginator {
        #[garde(range(min = 1, max = 50))]
        pub page_size: usize,
        #[garde(range(min = 1))]
        pub page_no: usize,
    }
    
    pub async fn paginator_from_query(Garde(Query(paginator)): Garde<Query<Paginator>>) {
        assert!((1..=50).contains(&paginator.page_size));
    }
  3. Use the `Valid<E>` extractor with `validator`

    main

    The Valid<E> extractor is used for validation powered by the validator crate. It is enabled by default in axum-valid.

    Installation

    cargo add validator --features derive
    cargo add axum-valid

    Usage

    Wrap an Axum extractor (like Query<T> or Json<T>) with Valid<E>. If the inner extractor succeeds but validation fails, the handler will return a 400 Bad Request with validation errors in the response body. If the inner extractor itself fails (e.g., malformed JSON), that rejection is returned first.

    All Valid<E> extractors support automatic dereferencing to access the inner data.

    use axum::extract::Query;
    use axum::Json;
    use axum_valid::Valid;
    use serde::Deserialize;
    use validator::Validate;
    
    #[derive(Debug, Validate, Deserialize)]
    pub struct Paginator {
        #[validate(range(min = 1, max = 50))]
        pub page_size: usize,
        #[validate(range(min = 1))]
        pub page_no: usize,
    }
    
    // Using with Query extractor
    pub async fn paginator_from_query(Valid(Query(paginator)): Valid<Query<Paginator>>) {
        // paginator is validated
    }
    
    // Using with Json extractor
    pub async fn paginator_from_json(paginator: Valid<Json<Paginator>>) {
        // paginator is validated and can be dereferenced
        println!("page_no: {}", paginator.page_no);
    }
  4. Use `Validify` extractors: `Validated<E>`, `Modified<E>`, `Validified<E>`, and `ValidifiedByRef<E>`

    main

    The validify integration provides extractors that support both validation and data modification (e.g., trimming strings or lowercasing them) during extraction.

    Installation

    For standard usage:

    cargo add validify
    cargo add axum-valid --features validify,basic --no-default-features

    For usage with axum-typed-multipart:

    cargo add axum_typed_multipart
    cargo add axum-valid --features validify,basic,typed_multipart --no-default-features

    Extractor Types

    1. Modified<E>: Returns the data after modification rules (like #[modify(trim)]) have been applied, but without performing validation. The data might still be invalid.
    2. Validated<E>: Performs validation on the extracted data. Used typically with Query.
    3. Validified<E>: Performs both modification and validation. Requires the inner type to implement validify::Payload. Used typically with Form or Json.
    4. ValidifiedByRef<E>: Similar to Validified, but used when the inner extractor does not use serde::Deserialize to construct data (e.g., TypedMultipart).

    Example

    use axum_valid::{Modified, Validated, Validified, ValidifiedByRef};
    use validify::{Payload, Validate, Validify};
    
    #[derive(Debug, Validify, Deserialize, Payload)]
    pub struct Parameters {
        #[modify(lowercase)]
        #[validate(length(min = 1, max = 50))]
        pub v0: String,
    }
    
    // Returns modified data, but might be invalid
    pub async fn parameters_from_json(modified_parameters: Modified<Json<Parameters>>) {}
    
    // Returns modified AND validated data
    pub async fn parameters_from_form(parameters: Validified<Form<Parameters>>) {}
    use axum::extract::Query;
    use axum::routing::{get, post};
    use axum::{Form, Json, Router};
    use axum_typed_multipart::{TryFromMultipart, TypedMultipart};
    use axum_valid::{Modified, Validated, Validified, ValidifiedByRef};
    use serde::Deserialize;
    use validify::{Payload, Validate, Validify};
    
    #[derive(Debug, Validify, Deserialize)]
    pub struct Paginator {
        #[validate(range(min = 1.0, max = 50.0))]
        pub page_size: usize,
        #[validate(range(min = 1.0))]
        pub page_no: usize,
    }
    
    #[derive(Debug, Validify, Deserialize, Payload)]
    pub struct Parameters {
        #[modify(lowercase)]
        #[validate(length(min = 1, max = 50))]
        pub v0: String,
        #[modify(trim)]
        #[validate(length(min = 1, max = 100))]
        pub v1: String,
    }
    
    pub async fn parameters_from_json(modified_parameters: Modified<Json<Parameters>>) {
        assert_eq!(modified_parameters.v0, modified_parameters.v0.to_lowercase());
    }
    
    pub async fn parameters_from_form(parameters: Validified<Form<Parameters>>) {
        assert!(parameters.validate().is_ok());
    }
  5. Understand `ValidifyRejection<E>`

    main

    When any of the validify extractors (Validated, Modified, Validified, or ValidifiedByRef) fail, they return a ValidifyRejection<E>.

    This type is an alias for ValidationRejection<ValidationErrors, E>. It can represent either:

    1. Valid(ValidationErrors): Errors encountered during the validify validation process.
    2. Inner(E): Errors encountered during the initial extraction of the inner type (e.g., a JSON parsing error).
  6. Understand ValidationRejection

    main

    The ValidationRejection<V, E> enum captures failures occurring during the validation process. It distinguishes between errors in the validation logic itself and errors from the underlying extractor.

    • Valid(V): Represents errors arising from the validation logic (e.g., a field failed a constraint). V is the error type.
    • Inner(E): Represents errors from the inner extractor (e.g., malformed JSON or missing fields). E is the inner error type.

    Response Behavior:

    • If the into_json feature is enabled, ValidationRejection::Valid(v) returns VALIDATION_ERROR_STATUS with the error v serialized as JSON.
    • If into_json is disabled, ValidationRejection::Valid(v) returns VALIDATION_ERROR_STATUS with the error converted to a string via Display.
  7. Understand `ValidRejection<E>` error handling

    main

    ValidRejection<E> is the error type returned when either Valid or ValidEx extractors fail. It is a type alias for ValidationRejection<ValidationErrors, E>.

    It can represent two failure states:

    1. Valid(ValidationErrors): The validation logic failed (e.g., a field did not meet constraints).
    2. Inner(E): The underlying extractor (like Json) failed before validation could even occur (e.g., malformed JSON).
    pub type ValidRejection<E> = ValidationRejection<ValidationErrors, E>;
  8. Troubleshoot axum-valid compilation issues

    main

    If you encounter compilation errors when using axum-valid, check the following three areas:

    1. Missing trait bounds: Ensure your types implement the necessary traits required by the extractors.
    2. Unmet feature requirements: Verify that the specific feature (e.g., garde, validify, or a specific format like yaml) is enabled in your Cargo.toml.
    3. Incorrect dependency versions: Check the Cargo.toml file in the axum-valid repository to ensure your versions of axum, serde, or other dependencies are compatible with the versions used by the crate.
  9. Use `Garde<E>` for validation with the `garde` backend

    main

    The Garde<E> extractor uses the garde validation library. It supports both standard validation and validation with context/arguments.

    To use Garde, you must disable default features of axum-valid and enable the garde and basic features. The context object must be provided via Axum's state. If using state, the validation function must be able to accept the context as an argument.

    use axum::routing::post;
    use axum::{Form, Router};
    use axum_valid::Garde;
    use garde::Validate;
    use serde::Deserialize;
    use std::net::SocketAddr;
    use std::ops::{RangeFrom, RangeInclusive};
    use tokio::net::TcpListener;
    
    #[derive(Debug, Validate, Deserialize)]
    #[garde(context(PaginatorValidContext))]
    pub struct Paginator {
        #[garde(custom(validate_page_size))]
        pub page_size: usize,
        #[garde(custom(validate_page_no))]
        pub page_no: usize,
    }
    
    fn validate_page_size(v: &usize, args: &PaginatorValidContext) -> garde::Result {
        args.page_size_range
            .contains(v)
            .then_some(())
            .ok_or_else(|| garde::Error::new("page_size is out of range"))
    }
    
    fn validate_page_no(v: &usize, args: &PaginatorValidContext) -> garde::Result {
        args.page_no_range
            .contains(v)
            .then_some(())
            .ok_or_else(|| garde::Error::new("page_no is out of range"))
    }
    
    #[derive(Debug, Clone)]
    pub struct PaginatorValidContext {
        page_size_range: RangeInclusive<usize>,
        page_no_range: RangeFrom<usize>,
    }
    
    pub async fn paginator_from_form_garde(Garde(Form(paginator)): Garde<Form<Paginator>>) {
        assert!((1..=50).contains(&paginator.page_size));
        assert!((1..).contains(&paginator.page_no));
    }
    
    #[tokio::main]
    async fn main() -> anyhow::Result<()> {
        let router = Router::new()
            .route("/form", post(paginator_from_form_garde))
            .with_state(PaginatorValidContext {
                page_size_range: 1..=50,
                page_no_range: 1..,
            });
    
        let listener = TcpListener::bind(&SocketAddr::from(([0u8, 0, 0, 0], 0u16))).await?;
        axum::serve(listener, router.into_make_service()).await?;
        Ok(())
    }
  10. Use `ValidEx<E>` for validation with arguments

    main

    Use the ValidEx<E> extractor when your validation logic requires external context or arguments. This is specifically designed for types that implement validator::ValidateArgs instead of the standard validator::Validate (which happens when using #[validate(context = ...)] in the validator crate).

    To use this, ensure you have the validator crate with the derive feature enabled and axum-valid installed. The context object must be provided via Axum's state (e.g., using .with_state()) and should implement FromRef if stored within a larger state struct.

    use axum::routing::post;
    use axum::{Form, Router};
    use axum_valid::ValidEx;
    use serde::Deserialize;
    use std::net::SocketAddr;
    use std::ops::{RangeFrom, RangeInclusive};
    use tokio::net::TcpListener;
    use validator::{Validate, ValidationError};
    
    #[derive(Debug, Validate, Deserialize)]
    #[validate(context = PaginatorValidArgs)]
    pub struct Paginator {
        #[validate(custom(function = "validate_page_size", use_context))]
        pub page_size: usize,
        #[validate(custom(function = "validate_page_no", use_context))]
        pub page_no: usize,
    }
    
    fn validate_page_size(v: usize, args: &PaginatorValidArgs) -> Result<(), ValidationError> {
        args.page_size_range
            .contains(&v)
            .then_some(())
            .ok_or_else(|| ValidationError::new("page_size is out of range"))
    }
    
    fn validate_page_no(v: usize, args: &PaginatorValidArgs) -> Result<(), ValidationError> {
        args.page_no_range
            .contains(&v)
            .then_some(())
            .ok_or_else(|| ValidationError::new("page_no is out of range"))
    }
    
    #[derive(Debug, Clone)]
    pub struct PaginatorValidArgs {
        page_size_range: RangeInclusive<usize>,
        page_no_range: RangeFrom<usize>,
    }
    
    pub async fn paginator_from_form_ex(ValidEx(Form(paginator)): ValidEx<Form<Paginator>>) {
        assert!((1..=50).contains(&paginator.page_size));
        assert!((1..).contains(&paginator.page_no));
    }
    
    #[tokio::main]
    async fn main() -> anyhow::Result<()> {
        let router = Router::new()
            .route("/form", post(paginator_from_form_ex))
            .with_state(PaginatorValidArgs {
                page_size_range: 1..=50,
                page_no_range: 1..,
            });
    
        let listener = TcpListener::bind(&SocketAddr::from(([0u8, 0, 0, 0], 0u16))).await?;
        axum::serve(listener, router.into_make_service()).await?;
        Ok(())
    }
  11. Configure the validation error HTTP status code

    main

    The HTTP status code returned when validation fails depends on the enabled features:

    • If the 422 feature is enabled, VALIDATION_ERROR_STATUS is set to StatusCode::UNPROCESSABLE_ENTITY (422).
    • If the 422 feature is not enabled, VALIDATION_ERROR_STATUS defaults to StatusCode::BAD_REQUEST (400).
    pub const VALIDATION_ERROR_STATUS: StatusCode = ...