[go: up one dir, main page]

Crate serde_qs

Source
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 to None)
  • 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_qs with axum.
helpers
A few common utility functions for encoding and decoding query strings
warp
Functionality for using serde_qs with warp.

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.