Expand description
Structured query-string decoding for Query<T>
(issue #1972).
See the extractors guide for the narrative version, with worked examples.
§Why this exists
Query<T> used to delegate straight to
serde_urlencoded, which is strictly
flat: it decodes ?q=foo&page=2 and nothing else. A Vec<String> field
fed the repeated-key form ?tags=a&tags=b failed with invalid type: string "a", expected a sequence, and a nested struct field was unrepresentable by
any encoding. That left MCP tool contracts unhonorable — tools/call
dispatch renders an array query argument as repeated keys, so a tool whose
inputSchema advertised tags: array dispatched a request its own handler
rejected — and pushed builders onto comma-separated strings and
JSON-in-a-string fields.
§Wire format
The decoder is a superset of the flat form: a query string with unique
scalar keys decodes exactly as it did before. On top of that it accepts a
bracketed dialect whose items[0][sku] shape matches the repeated-row
encoding nested_form renders — generalized here to
arbitrary objects, sequences and depths. Note this applies to the query
string only. Request bodies use flat keys: Form<T>
decodes through serde_urlencoded, and
ChangesetForm and
NestedChangesetForm decode
through from_urlencoded (repeated keys
fill a Vec, brackets stay part of the key).
q=foo // scalar
page=2 // scalar, coerced to the field's type
tags=a&tags=b // repeated key → sequence ["a", "b"]
tags[]=a&tags[]=b // append form → sequence ["a", "b"]
tags[0]=a&tags[2]=c // indexed form → sequence ["a", "c"]
filter[status]=open // nested object → { status: "open" }
items[0][sku]=A-1 // array of objectsPercent-encoded brackets (%5B / %5D) are the same thing: keys are
percent-decoded before parsing, so a client that encodes them round-trips
identically.
§Deliberate semantics
-
Scalar coercion matches
serde_urlencoded. Values arrive as text and are parsed withstr::parseinto the field’s type, sopage=2fills au32andflag=truefills abool. AnOption<T>field that is present but empty (?page=) still visitsSome, exactly asserde_urlencodeddoes — only an absent key isNone. -
A duplicated key is an error in a single-valued position.
?q=a&q=bagainst aStringfield is rejected —serde_urlencoded+ serde’s derive rejected it too (duplicate field), and quietly picking one of two values is how parameter-pollution bugs are built. A sequence field takes every occurrence; that is the whole point of the repeated-key form. -
Errors never echo a value. A decode failure names the field path and the expected type, never the submitted text — the message is returned in the 400 body and recorded by error reporters, and a query parameter can hold a secret.
-
Shape conflicts poison one key, not the request.
?filter=flat&filter[status]=openuses one name as both a scalar and a container. That key is rejected if the target claims it; a target that ignores it (ad-tracking junk, a crawler’s garbage parameter) still decodes, exactly as it did when the key was merely unrecognised. -
Malformed brackets stay literal. A key the bracket grammar cannot parse (
weird[unclosed) is used verbatim as a flat key, so a stray bracket never turns into a parse failure. -
Nesting is depth-capped at
MAX_DEPTHand indices key an ordered map rather than aVec, so neither deep nesting nor a huge index (tags[4000000000]=x) lets a request drive unbounded allocation. -
Map iteration is key-ordered. The tree keys each level with a
BTreeMap, so aQuery<Vec<(String, String)>>target sees pairs sorted by key rather than in submission order (occurrences of one key keep their order). Deterministic either way, but not the wire order.
§Compatibility note
Brackets are now structure, not part of the key text. A target that types
a parameter as a plain value — Query<HashMap<String, String>> is the
common one — used to receive ?filter[a]=1 as the literal key
"filter[a]"; it now sees a nested object and reports a decode error
naming the fix. Type such a field as a nested struct, or as
HashMap<String, serde_json::Value> to accept either shape.
Structs§
- Query
Error - A query-string decode failure, carrying the field path it occurred at.
Constants§
- MAX_
DEPTH - Maximum bracket nesting depth accepted in a query key.
Functions§
- from_
query_ str - Decode a raw (still percent-encoded) query string into
T.