Hi,
If you are a rust web dev beginner just like me, chances you will run into a task whereas you want to extract array of things from query param like this
../api/games?categories=arcade,puzzle,...
You will soon find out that axum predefined Query Extractor does not support that. It supports only one value for each param. Then you will run in to this Github issue which the idea is right but the sample code is outdated (or at least for me)
https://github.com/tokio-rs/axum/issues/434
https://github.com/tokio-rs/axum/discussions/1719
There are 2 solutions
- Use
axum-extra
crate - Implement your own custom extractor
These two different is as follows
- For
axum-extra
the url that from the client has to be in../api/games?categories=arcade,puzzle,...&
format. - For your own custom extractor, which you will use
serde-qs
, the url will be in the format of../api/games?categories[]=arcade&categories[]=puzzle,...
For me, the first option is cleaner (and easier to implement) but I will show you both option
1. Use axum-extra crate
This one is very easy. Go to your terminal and run cargo add axum-extra -F query
and the crate shall be installed.
Then in your route, just switch from common extractor to this one instead by adding use axum_extra::extract::Query;
instead of normal use axum::extract::Query;
Here's sample code
use axum::{http::StatusCode};
use serde::Deserialize;
use axum_extra::extract::Query;
use crate::{errors::app_error::AppError};
#[derive(Deserialize, Debug)]
pub struct GameParam {
pub categories: Vec<String>
}
pub async fn get_games(Query(params): Query<GameParam>) -> Result<StatusCode, AppError> {
let category = params.categories;
dbg!(&category);
Ok(StatusCode::OK)
}
2. Implement your own custom extractor
If by any chances, axum_extra is not enough for you, you can implement your own extractor
There are two way of implementing custom extractor. The different is that FromRequestParts will give you access to only request parts and FromRequest will give you access to the whole request body
FromRequestParts
Here is the code for extractor. Notice that we add generic T that must implement Deserialize trait
use axum::{
async_trait,
extract::FromRequestParts,
http::{
StatusCode,
request::Parts,
},
};
pub struct Qs2<T>(pub T);
#[async_trait]
impl<S, T> FromRequestParts<S> for Qs2<T>
where
S: Send + Sync,
T: serde::de::DeserializeOwned,
{
type Rejection = (StatusCode, &'static str);
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let query = parts.uri.query().unwrap();
Ok(Self(serde_qs::from_str(query).unwrap()))
}
}
Here's how we use it
use axum::{http::StatusCode};
use serde::Deserialize;
use crate::{errors::app_error::AppError, extractors::{qs2::Qs2}};
#[derive(Deserialize, Debug)]
pub struct GameParam {
pub categories: Vec<String>
}
pub async fn get_games(Qs2(params): Qs2<GameParam>) -> Result<StatusCode, AppError> {
let categories = params.categories;
dbg!(&categories);
Ok(StatusCode::OK)
}
Here's what the result looks like when we call ../games?categories[]=arcade&categories[]=puzzle
FromRequest
The is very similar to above, the only different is function signature.
use std::convert::Infallible;
use axum::{async_trait, extract::FromRequest, http::Request};
pub struct Qs<T>(pub T);
#[async_trait]
impl<S, B, T> FromRequest<S, B> for Qs<T>
where
// these bounds are required by `async_trait`
B: Send + 'static,
S: Send + Sync,
T: serde::de::DeserializeOwned,
{
type Rejection = Infallible;
async fn from_request(req: Request<B>, state: &S) -> Result<Self, Self::Rejection> {
let query = req.uri().query().unwrap();
Ok(Self(serde_qs::from_str(query).unwrap()))
}
}
use axum::{http::StatusCode};
use serde::Deserialize;
use crate::{errors::app_error::AppError, extractors::{qs::Qs}};
#[derive(Deserialize, Debug)]
pub struct GameParam {
pub categories: Vec<String>
}
pub async fn get_games(Qs(params): Qs<GameParam>) -> Result<StatusCode, AppError> {
let categories = params.categories;
dbg!(&categories);
Ok(StatusCode::OK)
}
That's it! Hope this help fellow Rust learning just like me out there!
Top comments (2)
axum query implements deref, given struct:
let search_query = search_query.deref().clone();
will return struct, I think the same applies to Vec. No need for axum_extra or custom extractor.
The Query implementation of axum uses serde_urlencoded and it would not support a key in your struct that would have the type
Vec<String>
for example.There is a test in the repository that show it will return an error for this use case : github.com/nox/serde_urlencoded/bl...