@@ -59,8 +59,10 @@ use crate::json;
5959use crate :: request:: Request ;
6060use crate :: response:: IntoResponse ;
6161use crate :: stream:: { StreamingBody , StreamingConfig } ;
62+ use crate :: validation:: Validatable ;
6263use bytes:: Bytes ;
6364use http:: { header, StatusCode } ;
65+ use rustapi_validate:: v2:: { AsyncValidate , ValidationContext } ;
6466
6567use serde:: de:: DeserializeOwned ;
6668use serde:: Serialize ;
@@ -253,7 +255,7 @@ impl<T> ValidatedJson<T> {
253255 }
254256}
255257
256- impl < T : DeserializeOwned + rustapi_validate :: Validate + Send > FromRequest for ValidatedJson < T > {
258+ impl < T : DeserializeOwned + Validatable + Send > FromRequest for ValidatedJson < T > {
257259 async fn from_request ( req : & mut Request ) -> Result < Self > {
258260 req. load_body ( ) . await ?;
259261 // First, deserialize the JSON body using simd-json when available
@@ -263,11 +265,8 @@ impl<T: DeserializeOwned + rustapi_validate::Validate + Send> FromRequest for Va
263265
264266 let value: T = json:: from_slice ( & body) ?;
265267
266- // Then, validate it
267- if let Err ( validation_error) = rustapi_validate:: Validate :: validate ( & value) {
268- // Convert validation error to API error with 422 status
269- return Err ( validation_error. into ( ) ) ;
270- }
268+ // Then, validate it using the unified Validatable trait
269+ value. do_validate ( ) ?;
271270
272271 Ok ( ValidatedJson ( value) )
273272 }
@@ -299,6 +298,110 @@ impl<T: Serialize> IntoResponse for ValidatedJson<T> {
299298 }
300299}
301300
301+ /// Async validated JSON body extractor
302+ ///
303+ /// Parses the request body as JSON, deserializes into type `T`, and validates
304+ /// using the `AsyncValidate` trait from `rustapi-validate`.
305+ ///
306+ /// This extractor supports async validation rules, such as database uniqueness checks.
307+ ///
308+ /// # Example
309+ ///
310+ /// ```rust,ignore
311+ /// use rustapi_rs::prelude::*;
312+ /// use rustapi_validate::v2::prelude::*;
313+ ///
314+ /// #[derive(Deserialize, Validate, AsyncValidate)]
315+ /// struct CreateUser {
316+ /// #[validate(email)]
317+ /// email: String,
318+ ///
319+ /// #[validate(async_unique(table = "users", column = "email"))]
320+ /// username: String,
321+ /// }
322+ ///
323+ /// async fn register(AsyncValidatedJson(body): AsyncValidatedJson<CreateUser>) -> impl IntoResponse {
324+ /// // body is validated asynchronously (e.g. checked existing email in DB)
325+ /// }
326+ /// ```
327+ #[ derive( Debug , Clone , Copy , Default ) ]
328+ pub struct AsyncValidatedJson < T > ( pub T ) ;
329+
330+ impl < T > AsyncValidatedJson < T > {
331+ /// Create a new AsyncValidatedJson wrapper
332+ pub fn new ( value : T ) -> Self {
333+ Self ( value)
334+ }
335+
336+ /// Get the inner value
337+ pub fn into_inner ( self ) -> T {
338+ self . 0
339+ }
340+ }
341+
342+ impl < T > Deref for AsyncValidatedJson < T > {
343+ type Target = T ;
344+
345+ fn deref ( & self ) -> & Self :: Target {
346+ & self . 0
347+ }
348+ }
349+
350+ impl < T > DerefMut for AsyncValidatedJson < T > {
351+ fn deref_mut ( & mut self ) -> & mut Self :: Target {
352+ & mut self . 0
353+ }
354+ }
355+
356+ impl < T > From < T > for AsyncValidatedJson < T > {
357+ fn from ( value : T ) -> Self {
358+ AsyncValidatedJson ( value)
359+ }
360+ }
361+
362+ impl < T : Serialize > IntoResponse for AsyncValidatedJson < T > {
363+ fn into_response ( self ) -> crate :: response:: Response {
364+ Json ( self . 0 ) . into_response ( )
365+ }
366+ }
367+
368+ impl < T : DeserializeOwned + AsyncValidate + Send + Sync > FromRequest for AsyncValidatedJson < T > {
369+ async fn from_request ( req : & mut Request ) -> Result < Self > {
370+ req. load_body ( ) . await ?;
371+
372+ let body = req
373+ . take_body ( )
374+ . ok_or_else ( || ApiError :: internal ( "Body already consumed" ) ) ?;
375+
376+ let value: T = json:: from_slice ( & body) ?;
377+
378+ // Create validation context from request
379+ // TODO: Extract validators from App State
380+ let ctx = ValidationContext :: default ( ) ;
381+
382+ // Perform full validation (sync + async)
383+ if let Err ( errors) = value. validate_full ( & ctx) . await {
384+ // Convert v2 ValidationErrors to ApiError
385+ let field_errors: Vec < crate :: error:: FieldError > = errors
386+ . fields
387+ . iter ( )
388+ . flat_map ( |( field, errs) | {
389+ let field_name = field. to_string ( ) ;
390+ errs. iter ( ) . map ( move |e| crate :: error:: FieldError {
391+ field : field_name. clone ( ) ,
392+ code : e. code . to_string ( ) ,
393+ message : e. message . clone ( ) ,
394+ } )
395+ } )
396+ . collect ( ) ;
397+
398+ return Err ( ApiError :: validation ( field_errors) ) ;
399+ }
400+
401+ Ok ( AsyncValidatedJson ( value) )
402+ }
403+ }
404+
302405/// Query string extractor
303406///
304407/// Parses the query string into type `T`.
0 commit comments