This post documents what I learned about creating custom Axum extractor which can be used with the validator
for validation.
Create a new test project:
β cargo new request-validator
cd request-validator
The first crate to be added is axum
for Axum framework.
request-validator on ξ main [?] via π¦ v1.79.0
β cargo add axum
Updating crates.io index
Adding axum v0.7.5 to dependencies
Features:
+ form
+ http1
+ json
+ matched-path
+ original-uri
+ query
+ tokio
+ tower-log
+ tracing
- __private_docs
- http2
- macros
- multipart
- ws
Updating crates.io index
Locking 82 packages to latest compatible versions
...
We also need tokio
crate that works together with axum
.
β cargo add tokio
Updating crates.io index
Adding tokio v1.38.0 to dependencies
Features:
- bytes
- fs
- full
- io-std
- io-util
- libc
- macros
- mio
- net
- num_cpus
- parking_lot
- process
- rt
- rt-multi-thread
- signal
- signal-hook-registry
- socket2
- sync
- test-util
- time
- tokio-macros
- tracing
- windows-sys
There are two features in tokio
crate that we’re interested in: macros
and rt-multi-thread
, which are to be used behind
the scene by Axum.
request-validator on ξ main [?] via π¦ v1.79.0
β cargo add tokio -F macros -F rt-multi-thread
Updating crates.io index
Adding tokio v1.38.0 to dependencies
Features:
+ macros
+ num_cpus
+ rt
+ rt-multi-thread
+ tokio-macros
- bytes
- fs
- full
- io-std
- io-util
- libc
- mio
- net
- parking_lot
- process
- signal
- signal-hook-registry
- socket2
- sync
- test-util
- time
- tracing
- windows-sys
Locking 2 packages to latest compatible versions
Adding hermit-abi v0.3.9 (latest: v0.4.0)
Adding num_cpus v1.16.0
I also need serde
crate for serializing and deserializing structs.
β cargo add serde --features derive
Updating crates.io index
Adding serde v1.0.204 to dependencies
Features:
+ derive
+ serde_derive
+ std
- alloc
- rc
- unstable
Next, I’m going to create a new struct called RequestUser
for this example code.
#[derive(Deserialize)]
pub struct RequestUser {
pub username: String,
pub password: String,
}
I want to implement the custom extractor on the above struct. Here are some information about custom extractor:
An extractor is a type that implements FromRequest or FromRequestParts. Extractors are how you pick apart the incoming request to get the parts your handler needs.
In Axum document, click on Modules, then click on extractor. Check the Implemnting FromRequest section.
If your extractor needs to consume the request body you must implement FromRequest.
We are going to implement this ourselves manually. Further check the document: Click on FromRequest.
impl<T, S> FromRequest<S> for Json<T>
where
T: DeserializeOwned,
S: Send + Sync,
Then update my code. Since I don’t use generic type in my RequestUser
struct, the generic T
can be dropped.
#[async_trait]
impl<S> FromRequest<S> for RequestUser
where
S: Send + Sync,
{
type Rejection = (StatusCode, String);
async fn from_request(req: Request, _state: &S) -> Result<Self, Self::Rejection> {
let Json(user) = req.extract::<Json<RequestUser>, _>()
.await
Ok(user)
}
}
But we still not haven’t yet implemented any validation. For this, we can use validator crate.
The validator
crate allows us to use macros to mark part of the struct for validating. This requires custom extractor (the whole point
of why the custom extractor is implemented above.)
Let’s update the validator
crate with derive
feature to the application.
β cargo add validator --features derive
Updating crates.io index
Adding validator v0.18.1 to dependencies
Features:
+ derive
+ validator_derive
- card
- card-validate
- indexmap
- unic
- unic-ucd-common
We can now update the RequestUser
struct to add validator as well as the messages when the eror occurs.
#[derive(Deserialize, Validate)]
pub struct RequestUser {
#[validate(email(message = "must be a valid email"))]
pub username: String,
#[validate(length(min = 8, message = "must have at least 8 characters"))]
pub password: String,
}
When we run the validate method once the user extracted out, we want to be shown the error messages if they exist.
#[async_trait]
impl<S> FromRequest<S> for RequestUser
where
S: Send + Sync,
{
type Rejection = (StatusCode, String);
async fn from_request(req: Request, _state: &S) -> Result<Self, Self::Rejection> {
let Json(user) = req.extract::<Json<RequestUser>, _>()
.await
.map_err(|error| (StatusCode::BAD_REQUEST, format!("{}", error)))?;
Ok(user)
}
}
Let’s test the result:
β curl -w '\n' -4 localhost:3000/user -H 'Content-Type: application/json' \
-d '{"username": "kenno", "password": "1234"}'
password: must have at least 8 characters
username: must be a valid email
β curl -4 -w '\n' localhost:3000/user -H 'Content-Type: application/json' \
-d '{"username": "kenno@example.com", "password": "1234"}' -v
* Host localhost:3000 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying 127.0.0.1:3000...
* Connected to localhost (127.0.0.1) port 3000
> POST /user HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/8.6.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 53
>
< HTTP/1.1 400 Bad Request
< content-type: text/plain; charset=utf-8
< content-length: 41
< date: Fri, 12 Jul 2024 15:22:46 GMT
<
* Connection #0 to host localhost left intact
password: must have at least 8 characters
β curl -4 -w '\n' localhost:3000/user -H 'Content-Type: application/json' `
-d '{"username": "kenno@example.com", "password": "12345678"}' -v
* Host localhost:3000 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying 127.0.0.1:3000...
* Connected to localhost (127.0.0.1) port 3000
> POST /user HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/8.6.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 57
>
< HTTP/1.1 200 OK
< content-type: text/html; charset=utf-8
< content-length: 34
< date: Fri, 12 Jul 2024 15:23:56 GMT
<
* Connection #0 to host localhost left intact
<h1>Hello, kenno@example.com!</h1>
Note using the custom extractor, the custom handler will not run at all if the validation fails.
The source code for this example project is available at: https://github.com/kenno/request-validator.
Special thank to Brooks Builds for creating the tutorial on how to create custom extractor for Axum. I strongly recommend that you check out his Introduction to Axum. series.
References: