feat(axum-extra): support deserializing Vec<Enum> from repeated query parameters#3751
Open
Zelys-DFKH wants to merge 5 commits intotokio-rs:mainfrom
Open
feat(axum-extra): support deserializing Vec<Enum> from repeated query parameters#3751Zelys-DFKH wants to merge 5 commits intotokio-rs:mainfrom
Zelys-DFKH wants to merge 5 commits intotokio-rs:mainfrom
Conversation
…Vec<Enum> query deserialization
Implements support for deserializing repeated query parameters into enum variants,
enabling handlers to accept mixed parameter names that map to different enum variants.
For example, ?id=123&username=alice&id=456 deserializes into Vec<SearchFilter>
with alternating Id and Username variants in order.
Implementation uses a JSON-bridge approach: for each query parameter pair, creates
a single-entry JSON object { variant_name: value } and leverages serde's existing
enum deserialization machinery for variant discrimination.
Adds two new extractors:
- QueryList<T>: Deserializes all query parameters into Vec<T>
- OptionalQueryList<T>: Same as QueryList, returns empty vec when no parameters
Includes comprehensive test coverage for:
- Alternating and repeated enum variants
- Empty query strings
- Unknown variant names (400 Bad Request)
- Invalid type values (deserialization errors)
- Both sync (try_from_uri) and async (from_request_parts) extraction paths
Fixes tokio-rs#3696
- Remove duplicate #[allow(dead_code)] attribute on struct
- Replace useless format!("{}", ...) with direct .to_string()
- Inline format string variables: format!("id:{id}") instead of format!("id:{}", id)
Collaborator
|
Could you add some tests where the parameter values have special characters (like |
…ueryList Tests cover URL-encoded whitespace (%20 and +), ampersands (%26), equals signs (%3D), plus signs (%2B), and other special characters (slashes, colons, percent). Verifies form_urlencoded properly decodes values in all scenarios. Addresses review feedback from yanns on PR tokio-rs#3751.
yanns
reviewed
May 8, 2026
| .map(|(k, v)| (k.into_owned(), v.into_owned())) | ||
| .collect(); | ||
|
|
||
| let mut result = Vec::new(); |
Collaborator
There was a problem hiding this comment.
In case we keep pairs, can we use Vec with capacity of the pairs to avoid allocations?
|
|
||
| async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> { | ||
| let query = parts.uri.query().unwrap_or_default(); | ||
| let pairs: Vec<(String, String)> = form_urlencoded::parse(query.as_bytes()) |
Collaborator
There was a problem hiding this comment.
Do we need to create this temporary Vec or could we use iterator all way long?
| async fn correct_rejection_status_code() { | ||
| #[derive(Deserialize)] | ||
| #[allow(dead_code)] | ||
| #[derive(Deserialize)] |
Collaborator
There was a problem hiding this comment.
Nitpick: this change is not necessary.
Reformat the OptionalQueryList handler closure and .get() call to match rustfmt expectations. Splits multi-line closure into proper indentation and collapses single-line method chain onto one line.
Clippy warns against calling .to_string() on string literals. Use .to_owned() instead, which is the idiomatic way to convert &str to String.
Author
|
Thanks for catching that special character requirement! Added tests with URL-encoded whitespace and ampersand. Also fixed a couple of clippy warnings that showed up in the new run. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #3696
Summary
This PR adds two new extractors to handle a real pain point: query strings with mixed enum variants. You can now write handlers that accept
?id=123&username=alice&id=456and get back a properly-typedVec<SearchFilter>with all the variants in order.Problem
Axum's query parameter handling works great for flat structs. But if you need to accept different parameter names as different enum variants — like filtering by ID or username in the same request — you're stuck.
serde_html_formdoesn't support this pattern because it doesn't know that the parameter name should pick the enum variant.Your options were: hand-parse the query string, build a custom extractor, or restructure the API. None of them are fun.
Solution
Two new extractors:
QueryList<T>- Deserializes all query parameters intoVec<T>, whereTis an enum. The parameter name becomes the variant discriminator.OptionalQueryList<T>- Same asQueryList, but returns an empty vec if there are no parameters (no error on missing data).How it works
The trick is simple: for each query parameter pair, create a JSON object
{ variant_name: value }and let serde's enum deserializer do the work. Serde already knows how to pick the right enum variant from the object key. We just had to bridge the gap between query strings and JSON objects.The implementation also handles value types correctly:
id=123deserializes as the number 123 (not the string "123"), andactive=truebecomes a boolean, just like you'd expect from JSON semantics.Example
Testing
Nine tests cover the realistic cases:
?id=1&id=2&id=3)OptionalQueryListbehavior with and without parametersAll existing tests pass. No breakage.
Changes
axum-extra/src/extract/query.rs— AddedQueryList<T>,OptionalQueryList<T>, thedeserialize_pairhelper, and rejection typesaxum-extra/src/extract/mod.rs— Exported the new typesaxum-extra/Cargo.toml— Addedserde_jsondependency for the "query" featureNotes
FailedToDeserializeQueryStringrejection type for consistencyQuery/OptionalQueryso the API surface is familiarDeserializeOwnedenum — full type safetyid=123is a number,active=trueis a boolean,name=aliceis a stringNatural fit for axum-extra's extraction toolkit. The approach is simple and delegates the hard work to serde, which already knows how to deserialize enums correctly.