Crate axum_valid

source ·
Expand description

§axum-valid

crates.io crates.io download LICENSE dependency status GitHub Workflow Status Coverage Status

§📑 Overview

axum-valid is a library that provides data validation extractors for the Axum web framework. It integrates * validator*, garde and validify, three popular validation crates in the Rust ecosystem, to offer convenient validation and data handling extractors for Axum applications.

§🚀 Basic usage

§📦 Valid<E>

  • Install
cargo add validator --features derive
cargo add axum-valid
# validator is enabled by default
  • Example
use axum::extract::Query;
use axum::routing::{get, post};
use axum::{Json, Router};
use axum_valid::Valid;
use serde::Deserialize;
use std::net::SocketAddr;
use tokio::net::TcpListener;
use validator::Validate;

#[derive(Debug, Validate, Deserialize)]
pub struct Pager {
    #[validate(range(min = 1, max = 50))]
    pub page_size: usize,
    #[validate(range(min = 1))]
    pub page_no: usize,
}

pub async fn pager_from_query(Valid(Query(pager)): Valid<Query<Pager>>) {
    assert!((1..=50).contains(&pager.page_size));
    assert!((1..).contains(&pager.page_no));
}

pub async fn pager_from_json(pager: Valid<Json<Pager>>) {
    assert!((1..=50).contains(&pager.page_size));
    assert!((1..).contains(&pager.page_no));
    // NOTE: all extractors provided support automatic dereferencing
    println!("page_no: {}, page_size: {}", pager.page_no, pager.page_size);
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let router = Router::new()
        .route("/query", get(pager_from_query))
        .route("/json", post(pager_from_json));
    let listener = TcpListener::bind(&SocketAddr::from(([0u8, 0, 0, 0], 0u16))).await?;
    axum::serve(listener, router.into_make_service()).await?;
    Ok(())
}

In case of inner extractor errors, it will first return the Rejection from the inner extractor. When validation errors occur, the outer extractor will automatically return 400 with validation errors as the HTTP message body.

§📦 Garde<E>

  • Install
cargo add garde --features derive
cargo add axum-valid --features garde,basic --no-default-features
# excluding validator
  • Example
use axum::extract::{FromRef, Query, State};
use axum::routing::{get, post};
use axum::{Json, Router};
use axum_valid::Garde;
use garde::Validate;
use serde::Deserialize;
use std::net::SocketAddr;
use tokio::net::TcpListener;

#[derive(Debug, Validate, Deserialize)]
pub struct Pager {
    #[garde(range(min = 1, max = 50))]
    pub page_size: usize,
    #[garde(range(min = 1))]
    pub page_no: usize,
}

pub async fn pager_from_query(Garde(Query(pager)): Garde<Query<Pager>>) {
    assert!((1..=50).contains(&pager.page_size));
    assert!((1..).contains(&pager.page_no));
}

pub async fn pager_from_json(pager: Garde<Json<Pager>>) {
    assert!((1..=50).contains(&pager.page_size));
    assert!((1..).contains(&pager.page_no));
    println!("page_no: {}, page_size: {}", pager.page_no, pager.page_size);
}

pub async fn get_state(_state: State<MyState>) {}

#[derive(Debug, Clone, FromRef)]
pub struct MyState {
    state_field: i32,
    without_validation_arguments: (),
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let router = Router::new()
        .route("/query", get(pager_from_query))
        .route("/json", post(pager_from_json));

    // WARNING: If you are using Garde and also have a state,
    // even if that state is unrelated to Garde,
    // you still need to implement FromRef<StateType> for ().
    // Tip: You can add an () field to your state and derive FromRef for it.
    let router = router.route("/state", get(get_state)).with_state(MyState {
        state_field: 1,
        without_validation_arguments: (),
    });
    let listener = TcpListener::bind(&SocketAddr::from(([0u8, 0, 0, 0], 0u16))).await?;
    axum::serve(listener, router.into_make_service()).await?;
    Ok(())
}

§📦 Validated<E>, Modified<E>, Validified<E> and ValidifiedByRef<E>

  • Install
cargo add validify
cargo add axum-valid --features validify,basic --no-default-features
  • Example

Extra dependencies of this example:

cargo add axum_typed_multipart
cargo add axum-valid --features validify,basic,typed_multipart --no-default-features
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 std::net::SocketAddr;
use tokio::net::TcpListener;
use validify::{Payload, Validate, Validify};

#[derive(Debug, Validify, Deserialize)]
pub struct Pager {
    #[validate(range(min = 1.0, max = 50.0))]
    pub page_size: usize,
    #[validate(range(min = 1.0))]
    pub page_no: usize,
}

pub async fn pager_from_query(Validated(Query(pager)): Validated<Query<Pager>>) {
    assert!((1..=50).contains(&pager.page_size));
    assert!((1..).contains(&pager.page_no));
}

// Payload is now required for Validified. (Added in validify 1.3.0)
#[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()
    );
    assert_eq!(modified_parameters.v1, modified_parameters.v1.trim())
    // but modified_parameters may be invalid
}

// NOTE: missing required fields will be treated as validation errors.
pub async fn parameters_from_form(parameters: Validified<Form<Parameters>>) {
    assert_eq!(parameters.v0, parameters.v0.to_lowercase());
    assert_eq!(parameters.v1, parameters.v1.trim());
    assert!(parameters.validate().is_ok());
}

// NOTE: TypedMultipart doesn't using serde::Deserialize to construct data
// we should use ValidifiedByRef instead of Validified
#[derive(Debug, Validify, TryFromMultipart)]
pub struct FormData {
    #[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_typed_multipart(
    ValidifiedByRef(TypedMultipart(data)): ValidifiedByRef<TypedMultipart<FormData>>,
) {
    assert_eq!(data.v0, data.v0.to_lowercase());
    assert_eq!(data.v1, data.v1.trim());
    assert!(data.validate().is_ok());
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let router = Router::new()
        .route("/validated", get(pager_from_query))
        .route("/modified", post(parameters_from_json))
        .route("/validified", post(parameters_from_form))
        .route("/validified_by_ref", post(parameters_from_typed_multipart));
    let listener = TcpListener::bind(&SocketAddr::from(([0u8, 0, 0, 0], 0u16))).await?;
    axum::serve(listener, router.into_make_service()).await?;
    Ok(())
}

To see how each inner extractor can be used with validation extractors, please refer to the example in the documentation of the corresponding module.

§🚀 Argument-Based Validation

§📦 ValidEx<E>

  • Install
cargo add validator --features derive
cargo add axum-valid
# validator is enabled by default
  • Example
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};

// NOTE: When some fields use custom validation functions with arguments,
// `#[derive(Validate)]` will implement `ValidateArgs` instead of `Validate` for the type.
#[derive(Debug, Validate, Deserialize)]
#[validate(context = PagerValidArgs)] // context is required
pub struct Pager {
    #[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: &PagerValidArgs) -> 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: &PagerValidArgs) -> Result<(), ValidationError> {
    args.page_no_range
        .contains(&v)
        .then_some(())
        .ok_or_else(|| ValidationError::new("page_no is out of range"))
}

// NOTE: Clone is required, consider using Arc to reduce deep copying costs.
#[derive(Debug, Clone)]
pub struct PagerValidArgs {
    page_size_range: RangeInclusive<usize>,
    page_no_range: RangeFrom<usize>,
}

pub async fn pager_from_form_ex(ValidEx(Form(pager)): ValidEx<Form<Pager>>) {
    assert!((1..=50).contains(&pager.page_size));
    assert!((1..).contains(&pager.page_no));
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let router = Router::new()
        .route("/form", post(pager_from_form_ex))
        .with_state(PagerValidArgs {
            page_size_range: 1..=50,
            page_no_range: 1..,
        });
    // NOTE: The PagerValidArgs can also be stored in a XxxState,
    // make sure it implements FromRef<XxxState>.

    let listener = TcpListener::bind(&SocketAddr::from(([0u8, 0, 0, 0], 0u16))).await?;
    axum::serve(listener, router.into_make_service()).await?;
    Ok(())
}

§📦 Garde<E>

  • Install
cargo add garde
cargo add axum-valid --features garde,basic --no-default-features
# excluding validator
  • Example
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(PagerValidContext))]
pub struct Pager {
    #[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: &PagerValidContext) -> 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: &PagerValidContext) -> 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 PagerValidContext {
    page_size_range: RangeInclusive<usize>,
    page_no_range: RangeFrom<usize>,
}

pub async fn pager_from_form_garde(Garde(Form(pager)): Garde<Form<Pager>>) {
    assert!((1..=50).contains(&pager.page_size));
    assert!((1..).contains(&pager.page_no));
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let router = Router::new()
        .route("/form", post(pager_from_form_garde))
        .with_state(PagerValidContext {
            page_size_range: 1..=50,
            page_no_range: 1..,
        });
    // NOTE: The PagerValidContext can also be stored in a XxxState,
    // make sure it implements FromRef<XxxState>.
    // Consider using Arc to reduce deep copying costs.
    let listener = TcpListener::bind(&SocketAddr::from(([0u8, 0, 0, 0], 0u16))).await?;
    axum::serve(listener, router.into_make_service()).await?;
    Ok(())
}

Current module documentation predominantly showcases Valid examples, the usage of ValidEx is analogous.

§🗂️ Extractors List

ExtractorBackend / FeatureData’s trait boundFunctionalityBenefitsDrawbacks
Valid<E>validatorvalidator::ValidateValidation
ValidEx<E>validatorvalidator::ValidateArgsValidation with arguments
Garde<E>gardegarde::ValidateValidation with or without argumentsRequire empty tuple as the argument if use state
Validated<E>validifyvalidify::ValidateValidation
Modified<E>validifyvalidify::ModifyModification / Conversion to response
Validified<E>validifyvalidify::Validify, validify::ValidifyPayload and serde::DeserializeOwnedConstruction, modification, validationTreat missing fields as validation errorsOnly works with extractors using serde
ValidifiedByRef<E>validifyvalidify::Validate and validify::ModifyModification, validation

§⚙️ Features

FeatureDescriptionModuleDefaultExampleTests
defaultEnables validator and support for Query, Json and Formvalidator, query, json, form
validatorEnables validator (Valid, ValidEx)validator
gardeEnables garde (Garde)garde
validifyEnables validify (Validated, Modified, Validified, ValidifedByRef)validify
basicEnables support for Query, Json and Formquery, json, form
jsonEnables support for Jsonjson
queryEnables support for Queryquery
formEnables support for Formform
typed_headerEnables support for TypedHeader from axum-extratyped_header
typed_multipartEnables support for TypedMultipart and BaseMultipart from axum_typed_multiparttyped_multipart
msgpackEnables support for MsgPack and MsgPackRaw from axum-serdemsgpack
yamlEnables support for Yaml from axum-serdeyaml
xmlEnables support for Xml from axum-serdexml
tomlEnables support for Toml from axum-serdetoml
sonicEnables support for Sonic from axum-serdesonic
cborEnables support for Cbor from axum-serdecbor
extraEnables support for Cached, WithRejection from axum-extraextra
extra_typed_pathEnables support for T: TypedPath from axum-extraextra::typed_path
extra_queryEnables support for Query from axum-extraextra::query
extra_formEnables support for Form from axum-extraextra::form
extra_protobufEnables support for Protobuf from axum-extraextra::protobuf
all_extra_typesEnables support for all extractors above from axum-extraN/A
all_typesEnables support for all extractors aboveN/A
422Use 422 Unprocessable Entity instead of 400 Bad Request as the status code when validation failsVALIDATION_ERROR_STATUS
into_jsonValidation errors will be serialized into JSON format and returned as the HTTP bodyN/A
full_validatorEnables validator, all_types, 422 and into_jsonN/A
full_gardeEnables garde, all_types, 422 and into_json. Consider using default-features = false to exclude default validator supportN/A
full_gardeEnables validify, all_types, 422 and into_json. Consider using default-features = false to exclude default validator supportN/A
fullEnables all features aboveN/A
aideEnables support for aideN/A

§🔌 Compatibility

To determine the compatible versions of dependencies that work together, please refer to the dependencies listed in the Cargo.toml file. The version numbers listed there will indicate the compatible versions.

If you encounter code compilation problems, it could be attributed to either missing trait bounds, unmet feature requirements, or incorrect dependency version selections.

§📜 License

This project is licensed under the MIT License.

§📚 References

Re-exports§

Modules§

  • Support for Cbor<T>
  • Support for extractors from axum-extra
  • Support for Form<T>
  • Garde support
  • Support for Json<T>
  • Support for MsgPack<T> and MsgPackRaw<T> from axum-serde
  • Support for Path<T>
  • Support for Query<T>
  • Support for Sonic<T>
  • Support for Toml<T> from axum-serde
  • Support for TypedHeader<T>
  • Support for TypedMultipart<T> and BaseMultipart<T, R> from axum_typed_multipart
  • Validator support
  • Validify support
  • Support for Xml<T> from axum-serde
  • Support for Yaml<T> from axum-serde

Enums§

Constants§

Traits§

  • Trait for types that can supply a reference that can be validated.