Expand description
Serde support for querystring-style strings
This library provides serialization and deserialization of querystrings
with support for arbitrarily nested structures. Unlike serde_urlencoded,
which only handles flat key-value pairs, serde_qs supports complex nested
data using bracket notation (e.g., user[name]=John&user[age]=30).
§Why use serde_qs?
- Nested structure support: Serialize/deserialize complex structs and maps
- Array support: Handle vectors and sequences with indexed notation
- Framework integration: Built-in support for Actix-web, Axum, and Warp
- Compatible syntax: Works with
qs(JavaScript) and Rack (Ruby)
§Basic Usage
#[macro_use]
extern crate serde_derive;
extern crate serde_qs as qs;
#[derive(Debug, PartialEq, Deserialize, Serialize)]
struct Address {
city: String,
postcode: String,
}
#[derive(Debug, PartialEq, Deserialize, Serialize)]
struct QueryParams {
id: u8,
name: String,
address: Address,
phone: u32,
user_ids: Vec<u8>,
}
let params = QueryParams {
id: 42,
name: "Acme".to_string(),
phone: 12345,
address: Address {
city: "Carrot City".to_string(),
postcode: "12345".to_string(),
},
user_ids: vec![1, 2, 3, 4],
};
let rec_params: QueryParams = qs::from_str("\
name=Acme&id=42&phone=12345&address[postcode]=12345&\
address[city]=Carrot+City&user_ids[0]=1&user_ids[1]=2&\
user_ids[2]=3&user_ids[3]=4")
.unwrap();
assert_eq!(rec_params, params);
§Supported Types
serde_qs supports all serde-compatible types:
- Primitives: strings, integers (u8-u64, i8-i64), floats (f32, f64), booleans
- Strings: UTF-8 strings (invalid UTF-8 handling configurable)
- Bytes:
Vec<u8>and&[u8]for raw binary data - Collections:
Vec<T>,HashMap<K, V>,BTreeMap<K, V>, arrays - Options:
Option<T>(missing values deserialize toNone) - Structs: Named and tuple structs with nested fields
- Enums: Externally tagged, internally tagged, and untagged representations
Note: Top-level types must be structs or maps. Primitives and sequences cannot be deserialized at the top level. And untagged representations have some limitations (see Flatten Workaround section).
§Query-String vs Form Encoding
By default, serde_qs uses query-string encoding which is more permissive:
- Spaces encoded as
+ - Minimal percent-encoding (brackets remain unencoded)
- Example:
name=John+Doe&items[0]=apple
The main benefit of query-string encoding is that it allows for more compact representations of nested structures, and supports square brackets in key names.
Form encoding (application/x-www-form-urlencoded) is stricter:
- Spaces encoded as
%20 - Most special characters percent-encoded
- Example:
name=John%20Doe&items%5B0%5D=apple
Form encoding is useful for compability with HTML forms and other applications that eagerly encode brackets.
Configure encoding mode:
use serde_qs::Config;
// Use form encoding
let config = Config::new().use_form_encoding(true);
let qs = config.serialize_string(&my_struct)?;§UTF-8 Handling
By default, serde_qs requires valid UTF-8 in string values. If your data
may contain non-UTF-8 bytes, consider serializing to Vec<u8> instead of
String. Non-UTF-8 bytes in ignored fields will not cause errors.
#[derive(Deserialize)]
struct Data {
// This field can handle raw bytes
raw_data: Vec<u8>,
// This field requires valid UTF-8
text: String,
}§Helpers for Common Scenarios
The helpers module provides utilities for common patterns when working with
querystrings, particularly for handling delimited values within a single parameter.
§Comma-Separated Values
Compatible with OpenAPI 3.0 style=form parameters:
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Deserialize, Serialize)]
struct Query {
#[serde(with = "serde_qs::helpers::comma_separated")]
ids: Vec<u64>,
}
// Deserialize from comma-separated string
let query: Query = serde_qs::from_str("ids=1,2,3,4").unwrap();
assert_eq!(query.ids, vec![1, 2, 3, 4]);
// Serialize back to comma-separated
let qs = serde_qs::to_string(&query).unwrap();
assert_eq!(qs, "ids=1,2,3,4");§Other Delimiters
Also supports pipe (|) and space delimited values:
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Deserialize, Serialize)]
struct Query {
#[serde(with = "serde_qs::helpers::pipe_delimited")]
tags: Vec<String>,
#[serde(with = "serde_qs::helpers::space_delimited")]
words: Vec<String>,
}
let query: Query = serde_qs::from_str("tags=foo|bar|baz&words=hello+world").unwrap();
assert_eq!(query.tags, vec!["foo", "bar", "baz"]);
assert_eq!(query.words, vec!["hello", "world"]);§Custom Delimiters
For other delimiters, use the generic helper:
use serde::{Deserialize, Serialize};
use serde_qs::helpers::generic_delimiter::{deserialize, serialize};
#[derive(Debug, PartialEq, Deserialize, Serialize)]
struct Query {
#[serde(deserialize_with = "deserialize::<_, _, '.'>")]
#[serde(serialize_with = "serialize::<_, _, '.'>")]
versions: Vec<u8>,
}
let query: Query = serde_qs::from_str("versions=1.2.3").unwrap();
assert_eq!(query.versions, vec![1, 2, 3]);§Flatten/untagged workaround
A current known limitation
in serde is deserializing #[serde(flatten)] structs for formats which
are not self-describing. This includes query strings: 12 can be an integer
or a string, for example.
A similar issue exists for #[serde(untagged)] enums, and internally-tagged enums.
The default behavior using derive macros uses content buffers which defers to
deserialize_any for deserializing the inner type. This means that any string
parsing that should have happened in the deserializer will not happen,
and must be done explicitly by the user.
We suggest the following workaround:
extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate serde_qs as qs;
extern crate serde_with;
use serde_with::{serde_as, DisplayFromStr};
#[derive(Deserialize, Serialize, Debug, PartialEq)]
struct Query {
a: u8,
#[serde(flatten)]
common: CommonParams,
}
#[serde_as]
#[derive(Deserialize, Serialize, Debug, PartialEq)]
struct CommonParams {
#[serde_as(as = "DisplayFromStr")]
limit: u64,
#[serde_as(as = "DisplayFromStr")]
offset: u64,
#[serde_as(as = "DisplayFromStr")]
remaining: bool,
}
fn main() {
let params = "a=1&limit=100&offset=50&remaining=true";
let query = Query { a: 1, common: CommonParams { limit: 100, offset: 50, remaining: true } };
let rec_query: Result<Query, _> = qs::from_str(params);
assert_eq!(rec_query.unwrap(), query);
}§Use with actix_web extractors
The actix4, actix3 or actix2 features enable the use of serde_qs::actix::QsQuery, which
is a direct substitute for the actix_web::Query and can be used as an extractor:
fn index(info: QsQuery<Info>) -> Result<String> {
Ok(format!("Welcome {}!", info.username))
}Support for actix-web 4.0 is available via the actix4 feature.
Support for actix-web 3.0 is available via the actix3 feature.
Support for actix-web 2.0 is available via the actix2 feature.
§Use with warp filters
The warp feature enables the use of serde_qs::warp::query(), which
is a substitute for the warp::query::query() filter and can be used like this:
serde_qs::warp::query(Config::default())
.and_then(|info| async move {
Ok::<_, Rejection>(format!("Welcome {}!", info.username))
})
.recover(serde_qs::warp::recover_fn);Modules§
- actix
- Actix-web integration for
serde_qs. - axum
- Functionality for using
serde_qswithaxum. - helpers
- A few common utility functions for encoding and decoding query strings
- warp
- Functionality for using
serde_qswithwarp.
Structs§
- Config
- Configuration for serialization and deserialization behavior.
- Deserializer
- A deserializer for the querystring format.
- Serializer
- A serializer for the querystring format.
Enums§
- Error
- Error type for
serde_qs.
Functions§
- from_
bytes - Deserializes a querystring from a
&[u8]. - from_
str - Deserializes a querystring from a
&str. - to_
string - Serializes a value into a querystring.
- to_
writer - Serializes a value into a generic writer object.