# axum-valid [![crates.io](https://img.shields.io/crates/v/axum-valid)](https://crates.io/crates/axum-valid) [![crates.io download](https://img.shields.io/crates/d/axum-valid)](https://crates.io/crates/axum-valid) [![LICENSE](https://img.shields.io/badge/license-MIT-blue)](https://github.com/gengteng/axum-valid/blob/main/LICENSE) [![dependency status](https://deps.rs/repo/github/gengteng/axum-valid/status.svg)](https://deps.rs/repo/github/gengteng/axum-valid) [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/gengteng/axum-valid/.github/workflows/main.yml?branch=main)](https://github.com/gengteng/axum-valid/actions/workflows/ci.yml) [![Coverage Status](https://coveralls.io/repos/github/gengteng/axum-valid/badge.svg?branch=main)](https://coveralls.io/github/gengteng/axum-valid?branch=main) This crate provides data validation capabilities for Axum based on the `validator` and `garde` crates. `validator` support is included by default. To use `garde`, enable it via the `garde` feature. `garde` alone can also be enabled by using `default-features = false`. The `Valid` type enables validation using `validator` for extractors like `Json`, `Path`, `Query` and `Form`. For validations requiring extra arguments, the `ValidEx` type is offered. The `Garde` type supports both argument and non-argument validations using `garde` in a unified way. Additional extractors like `TypedHeader`, `MsgPack` and `Yaml` are also supported through optional features. Refer to `Features` for details. ## Basic usage ```shell cargo add axum-valid ``` ```rust,no_run #[cfg(feature = "validator")] mod validator_example { use validator::Validate; use serde::Deserialize; use axum_valid::Valid; use axum::extract::Query; use axum::{Json, Router}; use axum::routing::{get, post}; #[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>, ) { assert!((1..=50).contains(&pager.page_size)); assert!((1..).contains(&pager.page_no)); } pub async fn pager_from_json( pager: Valid>, ) { assert!((1..=50).contains(&pager.page_size)); assert!((1..).contains(&pager.page_no)); // NOTE: support automatic dereferencing println!("page_no: {}, page_size: {}", pager.page_no, pager.page_size); } pub fn router() -> Router { Router::new() .route("/query", get(pager_from_query)) .route("/json", post(pager_from_json)) } } #[cfg(feature = "garde")] mod garde_example { use garde::Validate; use serde::Deserialize; use axum_valid::Garde; use axum::extract::Query; use axum::{Json, Router}; use axum::routing::{get, post}; #[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>, ) { assert!((1..=50).contains(&pager.page_size)); assert!((1..).contains(&pager.page_no)); } pub async fn pager_from_json( pager: Garde>, ) { assert!((1..=50).contains(&pager.page_size)); assert!((1..).contains(&pager.page_no)); // NOTE: support automatic dereferencing println!("page_no: {}, page_size: {}", pager.page_no, pager.page_size); } pub fn router() -> Router { Router::new() .route("/query", get(pager_from_query)) .route("/json", post(pager_from_json)) } } #[tokio::main] async fn main() -> anyhow::Result<()> { use axum::Router; let router = Router::new(); #[cfg(feature = "validator")] let router = router.nest("/validator", validator_example::router()); #[cfg(feature = "garde")] let router = router.nest("/garde", garde_example::router()); axum::Server::bind(&([0u8, 0, 0, 0], 8080).into()) .serve(router.into_make_service()) .await?; Ok(()) } ``` When validation errors occur, the extractor will automatically return 400 with validation errors as the HTTP message body. To see how each extractor can be used with `Valid`, please refer to the example in the [documentation](https://docs.rs/axum-valid) of the corresponding module. ## Argument-Based Validation Here's a basic example of using the `ValidEx` extractor to validate data in a `Form` using arguments: ```rust,no_run #[cfg(feature = "validator")] mod validator_example { use axum::routing::post; use axum::{Form, Router}; use axum_valid::{Arguments, ValidEx}; use serde::Deserialize; use std::ops::{RangeFrom, RangeInclusive}; use validator::{Validate, ValidateArgs, ValidationError}; // NOTE: When some fields use custom validation functions with arguments, // `#[derive(Validate)]` will implement `ValidateArgs` instead of `Validate` for the type. // The validation arguments will be a tuple of all the field validation args. // In this example it is (&RangeInclusive, &RangeFrom). // For more detailed information and understanding of `ValidateArgs` and their argument types, // please refer to the `validator` crate documentation. #[derive(Debug, Validate, Deserialize)] pub struct Pager { #[validate(custom(function = "validate_page_size", arg = "&'v_a RangeInclusive"))] pub page_size: usize, #[validate(custom(function = "validate_page_no", arg = "&'v_a RangeFrom"))] pub page_no: usize, } fn validate_page_size(v: usize, args: &RangeInclusive) -> Result<(), ValidationError> { args.contains(&v) .then_some(()) .ok_or_else(|| ValidationError::new("page_size is out of range")) } fn validate_page_no(v: usize, args: &RangeFrom) -> Result<(), ValidationError> { args.contains(&v) .then_some(()) .ok_or_else(|| ValidationError::new("page_no is out of range")) } // NOTE: Clone is required #[derive(Debug, Clone)] pub struct PagerValidArgs { page_size_range: RangeInclusive, page_no_range: RangeFrom, } // NOTE: This implementation allows PagerValidArgs to be the second member of ValidEx, and provides arguments for actual validation. // The type mapping >::Args represents the combination of validators applied on each field of Pager. // get() method returns the validating arguments to be used during validation. impl<'a> Arguments<'a> for PagerValidArgs { type T = Pager; // NOTE: >::Args == (&RangeInclusive, &RangeFrom) fn get(&'a self) -> >::Args { (&self.page_size_range, &self.page_no_range) } } pub async fn pager_from_form_ex(ValidEx(Form(pager), _): ValidEx, PagerValidArgs>) { assert!((1..=50).contains(&pager.page_size)); assert!((1..).contains(&pager.page_no)); } pub fn router() -> 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. } } #[cfg(feature = "garde")] mod garde_example { use axum::Router; pub fn router() -> Router { Router::new() } } #[tokio::main] async fn main() -> anyhow::Result<()> { use axum::Router; let router = Router::new(); #[cfg(feature = "validator")] let router = router.nest("/validator", validator_example::router()); #[cfg(feature = "garde")] let router = router.nest("/garde", garde_example::router()); axum::Server::bind(&([0u8, 0, 0, 0], 8080).into()) .serve(router.into_make_service()) .await?; Ok(()) } ``` Current module documentation predominantly showcases `Valid` examples, the usage of `ValidEx` is analogous. ## Features | Feature | Description | Module | Default | Example | Tests | |------------------|------------------------------------------------------------------------------------------------------|-----------------------------------------|---------|---------|-------| | default | Enables support for `Path`, `Query`, `Json` and `Form` | [`path`], [`query`], [`json`], [`form`] | ✅ | ✅ | ✅ | | json | Enables support for `Json` | [`json`] | ✅ | ✅ | ✅ | | query | Enables support for `Query` | [`query`] | ✅ | ✅ | ✅ | | form | Enables support for `Form` | [`form`] | ✅ | ✅ | ✅ | | typed_header | Enables support for `TypedHeader` | [`typed_header`] | ❌ | ✅ | ✅ | | typed_multipart | Enables support for `TypedMultipart` and `BaseMultipart` from `axum_typed_multipart` | [`typed_multipart`] | ❌ | ✅ | ✅ | | msgpack | Enables support for `MsgPack` and `MsgPackRaw` from `axum-msgpack` | [`msgpack`] | ❌ | ✅ | ✅ | | yaml | Enables support for `Yaml` from `axum-yaml` | [`yaml`] | ❌ | ✅ | ✅ | | extra | Enables support for `Cached`, `WithRejection` from `axum-extra` | [`extra`] | ❌ | ✅ | ✅ | | extra_typed_path | Enables support for `T: TypedPath` from `axum-extra` | [`extra::typed_path`] | ❌ | ✅ | ✅ | | extra_query | Enables support for `Query` from `axum-extra` | [`extra::query`] | ❌ | ✅ | ✅ | | extra_form | Enables support for `Form` from `axum-extra` | [`extra::form`] | ❌ | ✅ | ✅ | | extra_protobuf | Enables support for `Protobuf` from `axum-extra` | [`extra::protobuf`] | ❌ | ✅ | ✅ | | all_extra_types | Enables support for all extractors above from `axum-extra` | N/A | ❌ | ✅ | ✅ | | all_types | Enables support for all extractors above | N/A | ❌ | ✅ | ✅ | | 422 | Use `422 Unprocessable Entity` instead of `400 Bad Request` as the status code when validation fails | [`VALIDATION_ERROR_STATUS`] | ❌ | ✅ | ✅ | | into_json | Validation errors will be serialized into JSON format and returned as the HTTP body | N/A | ❌ | ✅ | ✅ | | full | Enables all features | N/A | ❌ | ✅ | ✅ | ## Compatibility To determine the compatible versions of `axum-valid`, `axum-extra`, `axum-yaml` and other 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. ## License This project is licensed under the MIT License. ## References * [axum](https://crates.io/crates/axum) * [validator](https://crates.io/crates/validator) * [serde](https://crates.io/crates/serde) * [axum-extra](https://crates.io/crates/axum-extra) * [axum-yaml](https://crates.io/crates/axum-yaml) * [axum-msgpack](https://crates.io/crates/axum-msgpack) * [axum_typed_multipart](https://crates.io/crates/axum_typed_multipart)