Skip to main content

ferrox_validation/
extractor.rs

1use axum::{
2    async_trait,
3    extract::{FromRequest, Request, rejection::JsonRejection},
4    Json,
5};
6use serde::de::DeserializeOwned;
7use validator::Validate;
8use ferrox_errors::AppError;
9
10/// ValidatedJson is an Axum extractor that behaves like Zod's `.parse()`.
11/// It consumes the request body as JSON, parses it into type T, and immediately
12/// calls `.validate()` on it. If validation fails, it throws a formatted 400 Bad Request error.
13#[derive(Debug, Clone, Copy, Default)]
14pub struct ValidatedJson<T>(pub T);
15
16#[async_trait]
17impl<T, S> FromRequest<S> for ValidatedJson<T>
18where
19    T: DeserializeOwned + Validate,
20    S: Send + Sync,
21    Json<T>: FromRequest<S, Rejection = JsonRejection>,
22{
23    type Rejection = AppError;
24
25    async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
26        // First, extract the JSON normally. 
27        // If it's malformed JSON, axum returns a JsonRejection.
28        let Json(value) = Json::<T>::from_request(req, state)
29            .await
30            .map_err(|err| AppError::ValidationError(format!("Invalid JSON payload: {}", err)))?;
31
32        // Second, run the validator validations.
33        // This is the "AutoZod" phase.
34        value.validate().map_err(|err| {
35            // We can map validation errors nicely. For now, we stringify it.
36            AppError::ValidationError(format!("Validation failed: {}", err))
37        })?;
38
39        // If everything is correct, wrap it and return
40        Ok(ValidatedJson(value))
41    }
42}