{"id":47664286,"url":"https://github.com/techouse/qs_rust","last_synced_at":"2026-04-05T13:04:07.056Z","repository":{"id":346667245,"uuid":"1188919960","full_name":"techouse/qs_rust","owner":"techouse","description":"A query string encoding and decoding library for Rust. Ported from qs for JavaScript.","archived":false,"fork":false,"pushed_at":"2026-03-25T20:12:01.000Z","size":2096,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-26T03:40:25.594Z","etag":null,"topics":["qs","query-encoding","query-parser","query-string","rust","url-parsing","url-query"],"latest_commit_sha":null,"homepage":"https://techouse.github.io/qs_rust/qs_rust/","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/techouse.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":"CODE-OF-CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null},"funding":{"github":"techouse","custom":["https://paypal.me/ktusar"]}},"created_at":"2026-03-22T18:59:32.000Z","updated_at":"2026-03-25T20:12:05.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/techouse/qs_rust","commit_stats":null,"previous_names":["techouse/qs_rust"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/techouse/qs_rust","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/techouse%2Fqs_rust","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/techouse%2Fqs_rust/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/techouse%2Fqs_rust/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/techouse%2Fqs_rust/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/techouse","download_url":"https://codeload.github.com/techouse/qs_rust/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/techouse%2Fqs_rust/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31305809,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-02T09:48:21.550Z","status":"ssl_error","status_checked_at":"2026-04-02T09:48:19.196Z","response_time":89,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["qs","query-encoding","query-parser","query-string","rust","url-parsing","url-query"],"created_at":"2026-04-02T11:50:09.892Z","updated_at":"2026-04-02T11:50:10.837Z","avatar_url":"https://github.com/techouse.png","language":"Rust","readme":"# qs_rust\n\n![qs_rust](https://github.com/techouse/qs_rust/blob/main/logo.png?raw=true)\n\nA query string encoding and decoding library for Rust.\n\nPorted from [qs](https://www.npmjs.com/package/qs) for JavaScript.\n\n[![Crates.io Version](https://img.shields.io/crates/v/qs_rust)](https://crates.io/crates/qs_rust)\n[![Crates.io MSRV](https://img.shields.io/crates/msrv/qs_rust)](https://crates.io/crates/qs_rust)\n[![Crates.io Size](https://img.shields.io/crates/size/qs_rust)](https://crates.io/crates/qs_rust)\n[![Crates.io Downloads (recent)](https://img.shields.io/crates/dr/qs_rust)](https://crates.io/crates/qs_rust)\n[![Test](https://github.com/techouse/qs_rust/actions/workflows/test.yml/badge.svg)](https://github.com/techouse/qs_rust/actions/workflows/test.yml)\n[![codecov](https://codecov.io/gh/techouse/qs_rust/graph/badge.svg?token=DHq7RZTFAn)](https://codecov.io/gh/techouse/qs_rust)\n[![Codacy Badge](https://app.codacy.com/project/badge/Grade/51927280c1814424b844cad1eec67180)](https://app.codacy.com/gh/techouse/qs_rust/dashboard?utm_source=gh\u0026utm_medium=referral\u0026utm_content=\u0026utm_campaign=Badge_grade)\n[![GitHub](https://img.shields.io/github/license/techouse/qs_rust)](https://github.com/techouse/qs_rust/blob/main/LICENSE)\n[![GitHub Sponsors](https://img.shields.io/github/sponsors/techouse)](https://github.com/sponsors/techouse)\n[![GitHub Repo stars](https://img.shields.io/github/stars/techouse/qs_rust)](https://github.com/techouse/qs_rust/stargazers)\n\n## Highlights\n\n- Nested object and list support: `foo[bar][baz]=qux` ⇄ nested `Value::Object` / `Value::Array`\n- Multiple list formats: indices, brackets, repeat, and comma\n- Dot-notation support plus `decode_dot_in_keys` / `encode_dot_in_keys`\n- UTF-8 and Latin-1 charsets, optional charset sentinel support, and numeric-entity decoding\n- Explicit Rust hook surfaces for custom decoding, filtering, sorting, scalar encoding, and temporal serialization\n- Iterative decode, merge, compact, and encode paths for deep-input safety\n- Node-backed parity tests plus cross-port regressions and perf tooling checked into the repo\n\n## Installation\n\n```toml\n[dependencies]\nqs_rust = \"1.0.0\"\n```\n\nOptional `serde` support:\n\n```toml\n[dependencies]\nqs_rust = { version = \"1.0.0\", features = [\"serde\"] }\n```\n\nOptional temporal adapters:\n\n```toml\n[dependencies]\nqs_rust = { version = \"1.0.0\", features = [\"chrono\", \"time\"] }\n```\n\n## Quick Start\n\n```rust\nuse qs_rust::{decode, encode, DecodeOptions, EncodeOptions, ListFormat, Value};\n\nlet decoded = decode(\n    \"user[name]=alice\u0026tags[]=x\u0026tags[]=y\",\n    \u0026DecodeOptions::new(),\n)\n.unwrap();\n\nassert!(decoded.contains_key(\"user\"));\nassert!(decoded.contains_key(\"tags\"));\n\nlet value = Value::Object(\n    [\n        (\n            \"user\".to_owned(),\n            Value::Object([(\"name\".to_owned(), Value::String(\"alice\".to_owned()))].into()),\n        ),\n        (\n            \"tags\".to_owned(),\n            Value::Array(vec![\n                Value::String(\"x\".to_owned()),\n                Value::String(\"y\".to_owned()),\n            ]),\n        ),\n    ]\n    .into(),\n);\n\nlet encoded = encode(\n    \u0026value,\n    \u0026EncodeOptions::new().with_list_format(ListFormat::Brackets),\n)\n.unwrap();\n\nassert_eq!(encoded, \"user%5Bname%5D=alice\u0026tags%5B%5D=x\u0026tags%5B%5D=y\");\n```\n\nQuery-string decoding only produces `Null`, `String`, `Array`, and `Object`. Structured inputs passed to `encode` or `decode_pairs` may also contain `Bool`, numeric variants, and `Bytes`.\n\n## Decoding\n\n### Nested Objects, Depth, Prefixes, and Delimiters\n\n```rust\nuse qs_rust::{decode, DecodeOptions, Delimiter, Value};\n\nlet nested = decode(\"foo[bar][baz]=qux\", \u0026DecodeOptions::new()).unwrap();\nassert_eq!(\n    nested.get(\"foo\"),\n    Some(\u0026Value::Object(\n        [(\n            \"bar\".to_owned(),\n            Value::Object([(\"baz\".to_owned(), Value::String(\"qux\".to_owned()))].into()),\n        )]\n        .into(),\n    )),\n);\n\nlet depth_limited = decode(\n    \"a[b][c][d][e][f][g]=x\",\n    \u0026DecodeOptions::new().with_depth(1),\n)\n.unwrap();\nassert_eq!(\n    depth_limited.get(\"a\"),\n    Some(\u0026Value::Object(\n        [(\n            \"b\".to_owned(),\n            Value::Object([(\"[c][d][e][f][g]\".to_owned(), Value::String(\"x\".to_owned()))].into()),\n        )]\n        .into(),\n    )),\n);\n\nlet prefixed = decode(\n    \"?a=b\u0026c=d\",\n    \u0026DecodeOptions::new().with_ignore_query_prefix(true),\n)\n.unwrap();\nassert_eq!(prefixed.get(\"a\"), Some(\u0026Value::String(\"b\".to_owned())));\nassert_eq!(prefixed.get(\"c\"), Some(\u0026Value::String(\"d\".to_owned())));\n\nlet custom_delimiter = decode(\n    \"a=b;c=d\",\n    \u0026DecodeOptions::new().with_delimiter(Delimiter::String(\";\".to_owned())),\n)\n.unwrap();\nassert_eq!(custom_delimiter.get(\"a\"), Some(\u0026Value::String(\"b\".to_owned())));\nassert_eq!(custom_delimiter.get(\"c\"), Some(\u0026Value::String(\"d\".to_owned())));\n```\n\nBy default, decoding depth is `5`, parameter limit is `1000`, lists are compacted, and duplicate keys are combined into arrays.\n\n### Dots, Lists, Duplicates, and Scalar Values\n\n```rust\nuse qs_rust::{decode, DecodeOptions, Duplicates, Value};\n\nlet dotted = decode(\"a.b=c\", \u0026DecodeOptions::new().with_allow_dots(true)).unwrap();\nassert_eq!(\n    dotted.get(\"a\"),\n    Some(\u0026Value::Object([(\"b\".to_owned(), Value::String(\"c\".to_owned()))].into())),\n);\n\nlet decoded_dot_key = decode(\n    \"name%252Eobj.first=John\u0026name%252Eobj.last=Doe\",\n    \u0026DecodeOptions::new().with_decode_dot_in_keys(true),\n)\n.unwrap();\nassert_eq!(\n    decoded_dot_key.get(\"name.obj\"),\n    Some(\u0026Value::Object(\n        [\n            (\"first\".to_owned(), Value::String(\"John\".to_owned())),\n            (\"last\".to_owned(), Value::String(\"Doe\".to_owned())),\n        ]\n        .into(),\n    )),\n);\n\nlet list = decode(\"a[]=b\u0026a[]=c\", \u0026DecodeOptions::new()).unwrap();\nassert_eq!(\n    list.get(\"a\"),\n    Some(\u0026Value::Array(vec![\n        Value::String(\"b\".to_owned()),\n        Value::String(\"c\".to_owned()),\n    ])),\n);\n\nlet empty_list = decode(\n    \"foo[]\u0026bar=baz\",\n    \u0026DecodeOptions::new().with_allow_empty_lists(true),\n)\n.unwrap();\nassert_eq!(empty_list.get(\"foo\"), Some(\u0026Value::Array(vec![])));\n\nlet first = decode(\n    \"foo=bar\u0026foo=baz\",\n    \u0026DecodeOptions::new().with_duplicates(Duplicates::First),\n)\n.unwrap();\nassert_eq!(first.get(\"foo\"), Some(\u0026Value::String(\"bar\".to_owned())));\n\nlet comma = decode(\"a=b,c\", \u0026DecodeOptions::new().with_comma(true)).unwrap();\nassert_eq!(\n    comma.get(\"a\"),\n    Some(\u0026Value::Array(vec![\n        Value::String(\"b\".to_owned()),\n        Value::String(\"c\".to_owned()),\n    ])),\n);\n\nlet scalars = decode(\"a=15\u0026b=true\u0026c=null\", \u0026DecodeOptions::new()).unwrap();\nassert_eq!(scalars.get(\"a\"), Some(\u0026Value::String(\"15\".to_owned())));\nassert_eq!(scalars.get(\"b\"), Some(\u0026Value::String(\"true\".to_owned())));\nassert_eq!(scalars.get(\"c\"), Some(\u0026Value::String(\"null\".to_owned())));\n```\n\n### Charset Sentinels, Numeric Entities, and Strict Null Handling\n\n```rust\nuse qs_rust::{decode, Charset, DecodeOptions, Value};\n\nlet latin1 = decode(\n    \"a=%A7\",\n    \u0026DecodeOptions::new().with_charset(Charset::Iso88591),\n)\n.unwrap();\nassert_eq!(latin1.get(\"a\"), Some(\u0026Value::String(\"§\".to_owned())));\n\nlet utf8_sentinel = decode(\n    \"utf8=%E2%9C%93\u0026a=%C3%B8\",\n    \u0026DecodeOptions::new()\n        .with_charset(Charset::Iso88591)\n        .with_charset_sentinel(true),\n)\n.unwrap();\nassert_eq!(utf8_sentinel.get(\"a\"), Some(\u0026Value::String(\"ø\".to_owned())));\n\nlet numeric_entities = decode(\n    \"a=%26%239786%3B\",\n    \u0026DecodeOptions::new()\n        .with_charset(Charset::Iso88591)\n        .with_interpret_numeric_entities(true),\n)\n.unwrap();\nassert_eq!(numeric_entities.get(\"a\"), Some(\u0026Value::String(\"☺\".to_owned())));\n\nlet strict_null = decode(\n    \"a\u0026b=\",\n    \u0026DecodeOptions::new().with_strict_null_handling(true),\n)\n.unwrap();\nassert_eq!(strict_null.get(\"a\"), Some(\u0026Value::Null));\nassert_eq!(strict_null.get(\"b\"), Some(\u0026Value::String(String::new())));\n```\n\n### Structured Input With `decode_pairs`\n\n```rust\nuse qs_rust::{decode_pairs, DecodeOptions, Value};\n\nlet decoded = decode_pairs(\n    vec![\n        (\"a[b]\".to_owned(), Value::String(\"1\".to_owned())),\n        (\"a[b]\".to_owned(), Value::String(\"2\".to_owned())),\n    ],\n    \u0026DecodeOptions::new(),\n)\n.unwrap();\n\nassert_eq!(\n    decoded.get(\"a\"),\n    Some(\u0026Value::Object([(\n        \"b\".to_owned(),\n        Value::Array(vec![\n            Value::String(\"1\".to_owned()),\n            Value::String(\"2\".to_owned()),\n        ]),\n    )]\n    .into())),\n);\n```\n\n`decode_pairs` starts at the structured merge pipeline and intentionally bypasses raw query-string behaviors such as delimiter splitting, query-prefix stripping, charset sentinel detection, and numeric-entity interpretation.\n\n## Encoding\n\n### Basics and Nested Objects\n\n```rust\nuse qs_rust::{encode, EncodeOptions, Value};\n\nlet simple = Value::Object([(\"a\".to_owned(), Value::String(\"b\".to_owned()))].into());\nassert_eq!(encode(\u0026simple, \u0026EncodeOptions::new()).unwrap(), \"a=b\");\n\nlet nested = Value::Object(\n    [(\n        \"a\".to_owned(),\n        Value::Object([(\"b\".to_owned(), Value::String(\"c\".to_owned()))].into()),\n    )]\n    .into(),\n);\nassert_eq!(encode(\u0026nested, \u0026EncodeOptions::new()).unwrap(), \"a%5Bb%5D=c\");\nassert_eq!(\n    encode(\u0026nested, \u0026EncodeOptions::new().with_encode(false)).unwrap(),\n    \"a[b]=c\"\n);\n```\n\n### List Formats\n\n```rust\nuse qs_rust::{encode, EncodeOptions, ListFormat, Value};\n\nlet data = Value::Object(\n    [(\n        \"a\".to_owned(),\n        Value::Array(vec![\n            Value::String(\"b\".to_owned()),\n            Value::String(\"c\".to_owned()),\n        ]),\n    )]\n    .into(),\n);\n\nassert_eq!(\n    encode(\u0026data, \u0026EncodeOptions::new().with_encode(false)).unwrap(),\n    \"a[0]=b\u0026a[1]=c\"\n);\nassert_eq!(\n    encode(\n        \u0026data,\n        \u0026EncodeOptions::new()\n            .with_encode(false)\n            .with_list_format(ListFormat::Brackets),\n    )\n    .unwrap(),\n    \"a[]=b\u0026a[]=c\"\n);\nassert_eq!(\n    encode(\n        \u0026data,\n        \u0026EncodeOptions::new()\n            .with_encode(false)\n            .with_list_format(ListFormat::Repeat),\n    )\n    .unwrap(),\n    \"a=b\u0026a=c\"\n);\nassert_eq!(\n    encode(\n        \u0026data,\n        \u0026EncodeOptions::new()\n            .with_encode(false)\n            .with_list_format(ListFormat::Comma),\n    )\n    .unwrap(),\n    \"a=b,c\"\n);\n```\n\n### Dot Notation, Empty Lists, Prefixes, and Delimiters\n\n```rust\nuse qs_rust::{encode, EncodeOptions, Value};\n\nlet dotted = Value::Object(\n    [(\n        \"a\".to_owned(),\n        Value::Object(\n            [(\n                \"b\".to_owned(),\n                Value::Object([(\"c\".to_owned(), Value::String(\"d\".to_owned()))].into()),\n            )]\n            .into(),\n        ),\n    )]\n    .into(),\n);\nassert_eq!(\n    encode(\n        \u0026dotted,\n        \u0026EncodeOptions::new()\n            .with_encode(false)\n            .with_allow_dots(true),\n    )\n    .unwrap(),\n    \"a.b.c=d\"\n);\n\nlet encoded_dot_key = Value::Object(\n    [(\n        \"name.obj\".to_owned(),\n        Value::Object(\n            [\n                (\"first\".to_owned(), Value::String(\"John\".to_owned())),\n                (\"last\".to_owned(), Value::String(\"Doe\".to_owned())),\n            ]\n            .into(),\n        ),\n    )]\n    .into(),\n);\nassert_eq!(\n    encode(\n        \u0026encoded_dot_key,\n        \u0026EncodeOptions::new()\n            .with_allow_dots(true)\n            .with_encode_dot_in_keys(true),\n    )\n    .unwrap(),\n    \"name%252Eobj.first=John\u0026name%252Eobj.last=Doe\"\n);\n\nlet empty_list = Value::Object(\n    [\n        (\"foo\".to_owned(), Value::Array(vec![])),\n        (\"bar\".to_owned(), Value::String(\"baz\".to_owned())),\n    ]\n    .into(),\n);\nassert_eq!(\n    encode(\n        \u0026empty_list,\n        \u0026EncodeOptions::new()\n            .with_encode(false)\n            .with_allow_empty_lists(true),\n    )\n    .unwrap(),\n    \"foo[]\u0026bar=baz\"\n);\n\nlet prefixed = Value::Object(\n    [\n        (\"a\".to_owned(), Value::String(\"b\".to_owned())),\n        (\"c\".to_owned(), Value::String(\"d\".to_owned())),\n    ]\n    .into(),\n);\nassert_eq!(\n    encode(\u0026prefixed, \u0026EncodeOptions::new().with_add_query_prefix(true)).unwrap(),\n    \"?a=b\u0026c=d\"\n);\nassert_eq!(\n    encode(\u0026prefixed, \u0026EncodeOptions::new().with_delimiter(\";\")).unwrap(),\n    \"a=b;c=d\"\n);\n```\n\n### Nulls, Bytes, Charset Sentinels, and RFC 1738 Formatting\n\n```rust\nuse qs_rust::{decode, encode, Charset, DecodeOptions, EncodeOptions, Format, Value};\n\nlet with_nulls = Value::Object(\n    [\n        (\"a\".to_owned(), Value::Null),\n        (\"b\".to_owned(), Value::String(String::new())),\n    ]\n    .into(),\n);\nassert_eq!(encode(\u0026with_nulls, \u0026EncodeOptions::new()).unwrap(), \"a=\u0026b=\");\nassert_eq!(\n    encode(\n        \u0026with_nulls,\n        \u0026EncodeOptions::new().with_strict_null_handling(true),\n    )\n    .unwrap(),\n    \"a\u0026b=\"\n);\n\nlet skip_nulls = Value::Object(\n    [\n        (\"a\".to_owned(), Value::String(\"b\".to_owned())),\n        (\"c\".to_owned(), Value::Null),\n    ]\n    .into(),\n);\nassert_eq!(\n    encode(\u0026skip_nulls, \u0026EncodeOptions::new().with_skip_nulls(true)).unwrap(),\n    \"a=b\"\n);\n\nlet bytes = Value::Object([(\"data\".to_owned(), Value::Bytes(vec![0x41, 0x20, 0xFF]))].into());\nassert_eq!(\n    encode(\n        \u0026bytes,\n        \u0026EncodeOptions::new().with_charset(Charset::Iso88591),\n    )\n    .unwrap(),\n    \"data=A%20%FF\"\n);\n\nlet latin1 = Value::Object([(\"æ\".to_owned(), Value::String(\"æ\".to_owned()))].into());\nassert_eq!(\n    encode(\n        \u0026latin1,\n        \u0026EncodeOptions::new().with_charset(Charset::Iso88591),\n    )\n    .unwrap(),\n    \"%E6=%E6\"\n);\n\nlet sentinel = Value::Object([(\"a\".to_owned(), Value::String(\"☺\".to_owned()))].into());\nassert_eq!(\n    encode(\u0026sentinel, \u0026EncodeOptions::new().with_charset_sentinel(true)).unwrap(),\n    \"utf8=%E2%9C%93\u0026a=%E2%98%BA\"\n);\n\nlet rfc1738 = Value::Object([(\"a\".to_owned(), Value::String(\"b c\".to_owned()))].into());\nassert_eq!(encode(\u0026rfc1738, \u0026EncodeOptions::new()).unwrap(), \"a=b%20c\");\nassert_eq!(\n    encode(\u0026rfc1738, \u0026EncodeOptions::new().with_format(Format::Rfc1738)).unwrap(),\n    \"a=b+c\"\n);\n\nlet round_trip = decode(\"a\u0026b=\", \u0026DecodeOptions::new().with_strict_null_handling(true)).unwrap();\nassert_eq!(round_trip.get(\"a\"), Some(\u0026Value::Null));\nassert_eq!(round_trip.get(\"b\"), Some(\u0026Value::String(String::new())));\n```\n\n## Customization\n\nThe sibling ports expose callback-heavy surfaces. In Rust those are available through explicit, typed hooks.\nRust does not expose a standalone public `Undefined` value; the sibling omission behavior is represented by\n`FilterResult::Omit` in encode callbacks.\n\nThe callback-free convenience layer is also part of the public encode surface:\n\n- `EncodeOptions::with_whitelist(...)` uses `WhitelistSelector::{Key, Index}` for key/index selection\n- `EncodeOptions::with_sort(...)` uses `SortMode::{Preserve, LexicographicAsc}` for built-in ordering\n- `Value::Object` uses the public `Object` alias, which is an ordered `IndexMap\u003cString, Value\u003e`\n\n### Custom Decode, Filter, Sort, and Encode Hooks\n\n`EncodeTokenEncoder` receives explicit `EncodeToken::{Key, Value, TextValue}` variants so Rust callers can distinguish key-path tokens from normal values and joined comma-list text.\n\n```rust\nuse qs_rust::{\n    decode, encode, DecodeDecoder, DecodeKind, DecodeOptions, EncodeFilter, EncodeOptions,\n    EncodeToken, EncodeTokenEncoder, FilterResult, FunctionFilter, Sorter, Value,\n};\n\nlet decode_options = DecodeOptions::new().with_decoder(Some(DecodeDecoder::new(\n    |raw, _charset, kind| match kind {\n        DecodeKind::Key =\u003e raw.to_owned(),\n        DecodeKind::Value =\u003e raw.to_ascii_uppercase(),\n    },\n)));\nlet decoded = decode(\"a=hello\", \u0026decode_options).unwrap();\nassert_eq!(decoded.get(\"a\"), Some(\u0026Value::String(\"HELLO\".to_owned())));\n\nlet filtered = Value::Object(\n    [\n        (\"b\".to_owned(), Value::String(\"2\".to_owned())),\n        (\"secret\".to_owned(), Value::String(\"x\".to_owned())),\n        (\"a\".to_owned(), Value::String(\"1\".to_owned())),\n    ]\n    .into(),\n);\nlet encoded = encode(\n    \u0026filtered,\n    \u0026EncodeOptions::new()\n        .with_encode(false)\n        .with_filter(Some(EncodeFilter::Function(FunctionFilter::new(\n            |prefix, _| {\n                if prefix.ends_with(\"secret\") {\n                    FilterResult::Omit\n                } else {\n                    FilterResult::Keep\n                }\n            },\n        ))))\n        .with_sorter(Some(Sorter::new(|left, right| left.cmp(right)))),\n)\n.unwrap();\nassert_eq!(encoded, \"a=1\u0026b=2\");\n\nlet numbers = Value::Object(\n    [\n        (\"b\".to_owned(), Value::I64(2)),\n        (\"a\".to_owned(), Value::I64(1)),\n    ]\n    .into(),\n);\nlet encoded_numbers = encode(\n    \u0026numbers,\n    \u0026EncodeOptions::new()\n        .with_encode(false)\n        .with_encoder(Some(EncodeTokenEncoder::new(|token, _, _| match token {\n            EncodeToken::Key(key) =\u003e key.to_owned(),\n            EncodeToken::Value(Value::I64(number)) =\u003e format!(\"n:{number}\"),\n            EncodeToken::Value(Value::String(text)) =\u003e text.clone(),\n            EncodeToken::TextValue(text) =\u003e text.to_owned(),\n            EncodeToken::Value(_) =\u003e String::new(),\n        })))\n        .with_sorter(Some(Sorter::new(|left, right| right.cmp(left)))),\n)\n.unwrap();\nassert_eq!(encoded_numbers, \"b=n:2\u0026a=n:1\");\n```\n\n### Temporal Values\n\n`qs_rust` now has a core temporal leaf:\n\n- `Value::Temporal(TemporalValue)`\n\nThe default formatter emits canonical ISO-8601 datetime text:\n\n- offset-aware values: `YYYY-MM-DDTHH:MM:SS[.fraction](Z|±HH:MM)`\n- naive values: `YYYY-MM-DDTHH:MM:SS[.fraction]`\n\nFor custom temporal output, use the core serializer hook:\n\n- `EncodeOptions::with_temporal_serializer(Some(TemporalSerializer::new(...)))`\n\nFeature-gated adapters remain available for converting native runtime types into\nthat core temporal model:\n\n- `chrono_support` behind the `chrono` feature\n- `time_support` behind the `time` feature\n\nThose helpers now produce `Value::Temporal(...)` directly, so temporal leaves can\nlive inside arbitrary nested arrays or objects without being pre-stringified.\n\n### Serde Bridge and Errors\n\nWith the `serde` feature enabled, `from_str(...)`, `to_string(...)`,\n`from_value(...)`, and `to_value(...)` all route typed data through the same\nsemantic core as the dynamic `Value` API.\n\nThat means plain query-string scalars arrive with the same semantics as `Value`: values such as `page=2` and `admin=true` decode as strings unless your serde model adds its own conversion layer.\n\nGeneric typed serde remains stringly for ordinary datetime-like fields too. If\nyou want typed models to preserve temporal leaves instead of collapsing them to\nstrings, use the opt-in helper modules under `qs_rust::serde::temporal::*`.\n\nFor a runnable typed example, use:\n\n```bash\ncargo run --example serde_bridge --features serde\n```\n\nCompared with `serde_qs`, `qs_rust` keeps the dynamic `qs` semantic core and\nlayers serde on top of it. For validated overlap cases, intentional\ndivergences such as stringly scalar decode and duplicate-key handling, and\n`serde_qs`-only extras that are out of scope for this bridge, see\n[docs/serde_comparison.md](https://github.com/techouse/qs_rust/blob/main/docs/serde_comparison.md).\n\n`DecodeError` and `EncodeError` are `#[non_exhaustive]`. Match them with a catch-all arm and prefer the stable inspector helpers (`is_*`, `*_limit()`) when you need durable error introspection.\n\n## Testing and Parity\n\nThe repository includes two Node-backed comparison layers:\n\n- `tests/comparison.rs` runs the checked-in smoke corpus from `tests/comparison/test_cases.json`\n- typed parity suites shell out to Node `qs` for per-case comparisons\n\nBefore running the Node-backed tests, bootstrap the fixture environment:\n\n```bash\ncd tests/comparison/js\nnpm ci\n```\n\nThe checked-in `package-lock.json` pins `qs` to `6.15.0`.\n\nRust-specific behavior lives alongside that parity layer:\n\n- `tests/regressions.rs` covers `decode_pairs`, `Bytes`, serde boundaries, deep stack-safety, and sibling-port-specific edge cases\n- `tests/properties_*.rs` cover randomized encode/decode/round-trip invariants\n- `tests/porting_ledger.md` records which Node/Python/Dart/Kotlin/C#/Swift cases were ported, skipped, or intentionally diverged\n\n## Fuzzing\n\nThe repository also includes a local-only `cargo-fuzz` harness for hostile-input hardening of the three public entrypoints:\n\n- `decode`\n- `encode`\n- `decode_pairs`\n\nThe fuzz targets are intentionally crash-focused for the first pass: successful results and clean `Err(...)` values are both acceptable. The goal is to catch panics, sanitizer failures, or obvious hang/regression cases on bounded inputs.\n\nInstall the tooling once:\n\n```bash\nrustup toolchain install nightly\ncargo install cargo-fuzz\n```\n\nBuild the fuzz targets:\n\n```bash\ncargo +nightly fuzz build decode\ncargo +nightly fuzz build encode\ncargo +nightly fuzz build decode_pairs\n```\n\nRun short local smoke sessions against a disposable copy of the committed corpus so libFuzzer does not spray generated inputs back into the tracked `fuzz/corpus/` tree:\n\n```bash\ntmpdir=\"$(mktemp -d /tmp/qs_rust_fuzz_decode.XXXXXX)\"\ncp -R fuzz/corpus/decode/. \"$tmpdir\"/\ncargo +nightly fuzz run decode \"$tmpdir\" -- -max_total_time=60 -verbosity=0 -print_final_stats=1\nrm -rf \"$tmpdir\"\n\ntmpdir=\"$(mktemp -d /tmp/qs_rust_fuzz_encode.XXXXXX)\"\ncp -R fuzz/corpus/encode/. \"$tmpdir\"/\ncargo +nightly fuzz run encode \"$tmpdir\" -- -max_total_time=60 -verbosity=0 -print_final_stats=1\nrm -rf \"$tmpdir\"\n\ntmpdir=\"$(mktemp -d /tmp/qs_rust_fuzz_decode_pairs.XXXXXX)\"\ncp -R fuzz/corpus/decode_pairs/. \"$tmpdir\"/\ncargo +nightly fuzz run decode_pairs \"$tmpdir\" -- -max_total_time=60 -verbosity=0 -print_final_stats=1\nrm -rf \"$tmpdir\"\n```\n\nRun a longer balanced soak with the checked-in helper script. By default it runs each target sequentially for `900` seconds, prints the exact command and temp paths it uses, and stops on the first non-zero exit:\n\n```bash\n./scripts/fuzz_soak.sh\n```\n\nThe helper script keeps generated corpora and crash artifacts under a disposable `/tmp` root instead of the tracked `fuzz/corpus/` tree. Useful knobs:\n\n```bash\n# Shorter local sanity pass.\nQS_FUZZ_SECONDS=60 ./scripts/fuzz_soak.sh\n\n# Target subset.\nQS_FUZZ_TARGETS=\"decode encode\" ./scripts/fuzz_soak.sh\n\n# Extra libFuzzer arguments, appended after the default balanced soak args.\nQS_FUZZ_ARGS=\"-jobs=1 -workers=1\" ./scripts/fuzz_soak.sh\n\n# Remove the temporary /tmp work tree after a successful run.\nQS_FUZZ_CLEANUP=1 ./scripts/fuzz_soak.sh\n```\n\nThe default balanced soak takes about `45` minutes across all three targets. The committed corpora live under `fuzz/corpus/` and use small JSON envelopes so new seeds can be added directly from README examples, parity cases, and regressions. Generated crashes and coverage output stay local in ignored paths under `fuzz/artifacts/` and `fuzz/coverage/`; disposable working corpora should stay in `/tmp` or another untracked directory.\n\nIf fuzzing finds a real issue, minimize it first with `cargo +nightly fuzz tmin ...`, then promote the minimized reproducer into a normal checked-in regression test before considering the bug closed.\n\n## Performance\n\nThe repository includes a local release-mode perf snapshot binary and checked-in baseline artifacts:\n\n```bash\ncargo run --release --bin qs_perf\ncargo run --release --bin qs_perf -- --scenario encode --format json\ncargo run --release --bin qs_perf -- --scenario decode --format json\npython3 scripts/capture_perf_baselines.py --scenario all\npython3 scripts/compare_perf_baseline.py --scenario all\npython3 scripts/cross_port_perf.py\n```\n\nThe harness, checked-in Rust baselines, and latest cross-port comparison snapshot all live in the repo now. Refresh those artifacts from a normal interactive shell when you want new numbers, and see [docs/performance.md](https://github.com/techouse/qs_rust/blob/main/docs/performance.md) for the trust-first capture workflow and failure-mode checks.\n\n## Stability Policy\n\nThis repository now tracks the published `1.0.0` contract. The intended `1.x` contract is the current public surface re-exported from `src/lib.rs`; changes to that surface should stay semver-compatible and only correct clear contract bugs or add clearly intended behavior.\n\nAfter `1.0.0`, changes should stay focused on bug fixes, test additions, documentation improvements, measurement-backed performance work, and additive features that keep the current `1.x` non-goals explicit.\n\n- Node `qs` `6.15.0` remains the semantic baseline for shared public query-string behavior.\n- C# remains the architectural reference for internal design decisions. Other sibling ports are informative, not normative.\n- The semantic core is shared across the dynamic API, the typed option/enums, the callback wrappers, and the optional `serde` bridge (`from_str` / `to_string`).\n- [docs/divergences.md](https://github.com/techouse/qs_rust/blob/main/docs/divergences.md) records the intentional `1.x` boundaries: host-object reflection, cycles, runtime bridge behavior, and other non-goals remain unsupported by design.\n- [docs/python_backend_readiness.md](https://github.com/techouse/qs_rust/blob/main/docs/python_backend_readiness.md) defines how the future `qs_codec` native backend should consume this crate and how the Python suite should validate `pure`, `rust`, and `auto` backends.\n- Merge, compact, finalization, and encode traversal are implemented iteratively to avoid recursion limits on deep inputs.\n\n## Support Policy\n\n- The crate-wide MSRV is Rust `1.88`.\n- The support target for `1.x` is latest stable Rust plus the MSRV on Linux, macOS, and Windows.\n- Optional features (`serde`, `chrono`, and `time`) follow the same support policy as the core crate. If a feature ever needs a newer compiler, the crate-wide MSRV should move with it instead of splitting policy.\n","funding_links":["https://github.com/sponsors/techouse","https://paypal.me/ktusar"],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftechouse%2Fqs_rust","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftechouse%2Fqs_rust","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftechouse%2Fqs_rust/lists"}