Unveiling Garde: Modern Validation in Rust with Trait-Based Design
Wenhao Wang
Dev Intern · Leapcell

Introduction
In the world of web services and data-intensive applications, ensuring the integrity and correctness of incoming data is paramount. Unvalidated input is a common source of security vulnerabilities, unexpected behavior, and ultimately, a poor user experience. While Rust's strong type system offers a foundational layer of safety, it doesn't inherently prevent invalid values within correctly typed data structures. This is where validation libraries become indispensable. They enable developers to define and enforce rules for data content, ensuring that only expected and valid information flows through their systems. Traditionally, validation in Rust has often involved boilerplate code or framework-specific solutions. This article introduces Garde, a modern, trait-based validation library that offers a fresh perspective on tackling this crucial challenge. We will explore its elegant design and demonstrate its practical utility within popular asynchronous web frameworks like Axum and Actix.
Understanding Garde's Core Concepts
Before diving into the practicalities, let's establish a clear understanding of the key concepts that underpin Garde's design.
Validation Traits
Central to Garde is the concept of validation traits. Instead of relying on a single, monolithic validation engine, Garde leverages Rust's powerful trait system. This means that validation rules are defined as traits, allowing for modularity, extensibility, and compile-time guarantees. Any type can implement a validation trait, thereby declaring its own validation logic. This decentralized approach makes it easy to compose complex validation schemes from simpler, reusable components.
Derive Macros
To simplify the process of implementing these validation traits, Garde provides powerful derive macros. These macros allow developers to annotate their structs and enums with attributes that automatically generate the necessary validation code. This significantly reduces boilerplate and improves readability, allowing developers to focus on defining the validation rules rather than writing repetitive implementation details.
Error Handling
Garde offers flexible error handling mechanisms. When validation fails, it generates a structured error report, making it easy to identify which specific rules were violated and why. This precise feedback is crucial for both debugging during development and providing meaningful error messages to API consumers.
Garde's Architecture and Implementation
Garde's design revolves around its Validate trait. Any type that needs to be validated must implement this trait. As mentioned, the #[derive(Validate)] macro simplifies this process.
Consider a simple user registration struct:
use garde::Validate; #[derive(Debug, Validate)] struct UserRegistration { #[garde(length(min = 3, max = 20))] #[garde(alpanum)] username: String, #[garde(email)] email: String, #[garde(length(min = 8))] #[garde(contains_digit)] #[garde(contains_uppercase)] password: String, }
In this example, the UserRegistration struct is annotated with #[derive(Validate)]. Each field then uses specific garde attributes to define its validation rules:
#[garde(length(min = 3, max = 20))]ensures theusernameis between 3 and 20 characters long.#[garde(alpanum)]ensures theusernamecontains only alphanumeric characters.#[garde(email)]validates theemailfield against a standard email format.#[garde(length(min = 8))],#[garde(contains_digit)], and#[garde(contains_uppercase)]enforce a strong password policy.
To perform validation, you would simply call the validate() method on an instance of the struct:
let valid_user = UserRegistration { username: "testuser".to_string(), email: "test@example.com".to_string(), password: "StrongPassword123".to_string(), }; assert!(valid_user.validate(&()).is_ok()); let invalid_user = UserRegistration { username: "a".to_string(), // Too short email: "invalid-email".to_string(), password: "weak".to_string(), }; assert!(valid_user.validate(&()).is_err());
The validate(&()) call takes a context parameter, which in this simple case is (). For more complex scenarios, you might pass in a context object containing database connections or other services needed for custom validation logic.
Application in Axum and Actix
Garde truly shines when integrated with web frameworks, where robust input validation is critical for handling API requests. Both Axum and Actix provide mechanisms to extract data from incoming requests and integrate custom validation logic.
Axum Integration
In Axum, Garde can be seamlessly integrated using a custom extractor. By implementing the FromRequestParts or FromRequest trait for a type that uses Garde's Validate macro, we can automatically validate incoming request bodies.
use axum::{ async_trait, extract::{FromRequest, rejection::FormRejection, Request}, http::StatusCode, response::{IntoResponse, Response}, Json, }; use serde::{Deserialize, Serialize}; use garde::Validate; #[derive(Debug, Deserialize, Serialize, Validate)] struct CreateUserPayload { #[garde(length(min = 3, max = 20))] #[garde(alpanum)] username: String, #[garde(email)] email: String, #[garde(length(min = 8))] #[garde(contains_digit)] #[garde(contains_uppercase)] password: String, } struct ValidatedJson<T: Validate>(T); #[async_trait] impl<T> FromRequest for ValidatedJson<T> where T: Deserialize<'static> + Validate, { type Rejection = AppError; async fn from_request(req: Request, state: &()) -> Result<Self, Self::Rejection> { let Json(payload) = Json::<T>::from_request(req, state) .await .map_err(AppError::AxumJsonRejection)?; payload.validate(&()) .map_err(|e| AppError::ValidationFailed(e))?; Ok(ValidatedJson(payload)) } } pub enum AppError { ValidationFailed(garde::Errors), AxumJsonRejection(axum::extract::rejection::JsonRejection), // Other application errors } impl IntoResponse for AppError { fn into_response(self) -> Response { match self { AppError::ValidationFailed(errors) => ( StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": "Validation failed", "details": errors.to_string() })), ).into_response(), AppError::AxumJsonRejection(rejection) => rejection.into_response(), } } } // In an Axum handler: async fn create_user(ValidatedJson(payload): ValidatedJson<CreateUserPayload>) -> impl IntoResponse { // If we reach here, the payload is already validated println!("Creating user with username: {}", payload.username); StatusCode::CREATED }
Here, ValidatedJson acts as a custom extractor that first deserializes the JSON request body and then uses Garde to validate it. If validation fails, an AppError::ValidationFailed is returned, which then translates into a 400 Bad Request response with detailed error messages.
Actix Web Integration
Actix Web also facilitates custom extractors, making Garde integration straightforward.
use actix_web::{ web::{self, Json}, Responder, HttpResponse, }; use serde::{Deserialize, Serialize}; use garde::Validate; #[derive(Debug, Deserialize, Serialize, Validate)] struct CreateProductPayload { #[garde(length(min = 5))] name: String, #[garde(range(min = 1.0, max = 1000.0))] price: f64, } async fn create_product(payload: Json<CreateProductPayload>) -> impl Responder { match payload.validate(&()) { Ok(_) => { println!("Creating product: {}", payload.name); HttpResponse::Created().json(payload.0) } Err(errors) => { HttpResponse::BadRequest().json(serde_json::json!({ "error": "Validation failed", "details": errors.to_string(), })) } } } // In your Actix app configuration: // config.service(web::resource("/products").route(web::post().to(create_product)));
In the Actix example, while not a full custom extractor like the Axum one (for brevity), the validation logic is clearly applied immediately after the Json extractor. The payload.validate(&()) call performs the validation, and based on the result, either processes the request or returns a 400 Bad Request with an error message derived from Garde's error structure. For a more idiomatic Actix integration, a custom FromRequest implementation similar to the Axum example could be created.
Conclusion
Garde stands out as a modern, trait-based validation library for Rust, offering a clear, concise, and highly composable way to enforce data integrity. Its reliance on Rust's trait system and powerful derive macros minimizes boilerplate and enhances code readability, while its flexible error handling provides detailed feedback. When integrated with web frameworks like Axum and Actix, Garde empowers developers to build more robust, secure, and maintainable applications by ensuring that only valid data enters their systems, making it an essential tool in any Rust developer's toolkit. Garde simplifies complex validation, fostering more reliable Rust applications.

