{"id":30346364,"url":"https://github.com/techouse/qs-swift","last_synced_at":"2026-04-04T13:27:24.466Z","repository":{"id":309580950,"uuid":"1036727762","full_name":"techouse/qs-swift","owner":"techouse","description":"A query string encoding and decoding library for Swift/ObjC. Ported from qs for JavaScript.","archived":false,"fork":false,"pushed_at":"2026-04-03T18:51:16.000Z","size":1348,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-03T20:25:06.007Z","etag":null,"topics":["objective-c","qs","query-encoding","query-string","swift","swiftpm","url-parsing","url-query"],"latest_commit_sha":null,"homepage":"https://techouse.github.io/qs-swift/","language":"Swift","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":"CONTRIBUTING.md","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":"2025-08-12T13:52:34.000Z","updated_at":"2026-03-31T10:55:20.000Z","dependencies_parsed_at":"2025-08-12T18:27:37.434Z","dependency_job_id":"f87788d2-6551-4681-b148-c1205a52d950","html_url":"https://github.com/techouse/qs-swift","commit_stats":null,"previous_names":["techouse/qs-swift"],"tags_count":17,"template":false,"template_full_name":null,"purl":"pkg:github/techouse/qs-swift","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/techouse%2Fqs-swift","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/techouse%2Fqs-swift/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/techouse%2Fqs-swift/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/techouse%2Fqs-swift/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/techouse","download_url":"https://codeload.github.com/techouse/qs-swift/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/techouse%2Fqs-swift/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31402263,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-04T10:20:44.708Z","status":"ssl_error","status_checked_at":"2026-04-04T10:20:06.846Z","response_time":60,"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":["objective-c","qs","query-encoding","query-string","swift","swiftpm","url-parsing","url-query"],"created_at":"2025-08-18T15:18:45.432Z","updated_at":"2026-04-04T13:27:24.454Z","avatar_url":"https://github.com/techouse.png","language":"Swift","funding_links":["https://github.com/sponsors/techouse","https://paypal.me/ktusar"],"categories":[],"sub_categories":[],"readme":"# QsSwift\n\n\u003cp align=\"center\"\u003e\n    \u003cimg src=\"https://github.com/techouse/qs-swift/raw/main/logo.png?raw=true\" width=\"256\" alt=\"QsSwift\" /\u003e\n\u003c/p\u003e\n\nA fast, flexible query string **encoding/decoding** library for Swift and [Objective-C](#objective-c).\n\nPorted from [qs](https://www.npmjs.com/package/qs) for JavaScript.\n\n[![SwiftPM version](https://img.shields.io/github/v/release/techouse/qs-swift?logo=swift\u0026label=SwiftPM)](https://github.com/techouse/qs-swift/releases/latest)\n[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Ftechouse%2Fqs-swift%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/techouse/qs-swift)\n[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Ftechouse%2Fqs-swift%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/techouse/qs-swift)\n[![Docs (Swift)](https://img.shields.io/badge/Docs-QsSwift-blue)](https://techouse.github.io/qs-swift/qsswift/documentation/qsswift/) [![Docs (ObjC)](https://img.shields.io/badge/Docs-QsObjC-blue)](https://techouse.github.io/qs-swift/qsobjc/documentation/qsobjc/)\n[![License](https://img.shields.io/github/license/techouse/qs-swift)](LICENSE)\n[![Test](https://github.com/techouse/qs-swift/actions/workflows/test.yml/badge.svg)](https://github.com/techouse/qs-swift/actions/workflows/test.yml)\n[![codecov](https://codecov.io/gh/techouse/qs-swift/graph/badge.svg?token=hk2eROAKOo)](https://codecov.io/gh/techouse/qs-swift)\n[![Codacy Badge](https://app.codacy.com/project/badge/Grade/7ebd5d6b9de243d79f05fa995f2a2299)](https://app.codacy.com/gh/techouse/qs-swift/dashboard?utm_source=gh\u0026utm_medium=referral\u0026utm_content=\u0026utm_campaign=Badge_grade)\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-swift)](https://github.com/techouse/qs-swift/stargazers)\n\n---\n\n## Highlights\n\n- Nested maps \u0026 lists: `foo[bar][baz]=qux` ⇄ `[\"foo\": [\"bar\": [\"baz\": \"qux\"]]]`\n- Multiple list formats (indices, brackets, repeat, comma)\n- Dot-notation (`a.b=c`) and optional dot-encoding (setting `decodeDotInKeys` automatically enables dot notation)\n- UTF‑8 and ISO‑8859‑1 charsets; optional charset sentinel (`utf8=✓`)\n- Custom encoders/decoders, sorting, filtering, strict/null handling\n- Deterministic ordering with `OrderedDictionary` (swift-collections)\n\n---\n\n## Requirements\n\n- Swift **5.10+**\n- Platforms: macOS **12+**, iOS **13+**, tvOS **13+**, watchOS **8+**\n\n---\n\n## Installation (Swift Package Manager)\n\n### Xcode\n\n- File → Add Package Dependencies…\n- Enter: https://github.com/techouse/qs-swift\n- Add the Qs library to your target.\n\n### Package.swift\n\n```swift\n// in your Package.swift\ndependencies: [\n    .package(url: \"https://github.com/techouse/qs-swift\", from: \"1.1.1\")\n],\ntargets: [\n    .target(\n        name: \"YourApp\",\n        dependencies: [\n            .product(name: \"QsSwift\", package: \"qs-swift\")\n        ]\n    )\n]\n```\n\n---\n\n## Quick start\n\n```swift\nimport QsSwift\n\n// Decode\nlet decoded: [String: Any] = try Qs.decode(\"foo[bar]=baz\u0026foo[list][]=a\u0026foo[list][]=b\")\n// decoded == [\"foo\": [\"bar\": \"baz\", \"list\": [\"a\", \"b\"]]]\n\n// Encode\nlet encoded: String = try Qs.encode([\"foo\": [\"bar\": \"baz\"]])\n// encoded == \"foo%5Bbar%5D=baz\"\n```\n\n---\n\n## Usage\n\n### Simple\n\n```swift\n// Decode\nlet obj: [String: Any] = try Qs.decode(\"a=c\")\n// [\"a\": \"c\"]\n\n// Encode\nlet qs: String = try Qs.encode([\"a\": \"c\"])\n// \"a=c\"\n```\n\n---\n\n## Decoding\n\n### Nested maps\n\n```swift\ntry Qs.decode(\"foo[bar]=baz\")\n// [\"foo\": [\"bar\": \"baz\"]]\n\ntry Qs.decode(\"a%5Bb%5D=c\")\n// [\"a\": [\"b\": \"c\"]]\n\ntry Qs.decode(\"foo[bar][baz]=foobarbaz\")\n// [\"foo\": [\"bar\": [\"baz\": \"foobarbaz\"]]]\n```\n\n### Depth (default: 5)\n\nBeyond the configured depth, the remainder is kept literally:\n\n```swift\nlet r = try Qs.decode(\"a[b][c][d][e][f][g][h][i]=j\")\n// r[\"a\"]?[\"b\"]?[\"c\"]?[\"d\"]?[\"e\"]?[\"f\"]?[\"[g][h][i]\"] == \"j\"\n```\n\nSet `strictDepth: true` to **throw** instead of collapsing the remainder when the limit is exceeded.\n\nOverride depth:\n\n```swift\nlet r = try Qs.decode(\"a[b][c][d][e][f][g][h][i]=j\", options: .init(depth: 1))\n// r[\"a\"]?[\"b\"]?[\"[c][d][e][f][g][h][i]\"] == \"j\"\n```\n\n### Parameter limit \u0026 ignoring `?`\n\n```swift\ntry Qs.decode(\"a=b\u0026c=d\", options: .init(parameterLimit: 1))\n// [\"a\": \"b\"]\n\ntry Qs.decode(\"?a=b\u0026c=d\", options: .init(ignoreQueryPrefix: true))\n// [\"a\": \"b\", \"c\": \"d\"]\n```\n\n### Custom delimiters (string or regex)\n\n```swift\ntry Qs.decode(\"a=b;c=d\", options: .init(delimiter: StringDelimiter(\";\")))\n// [\"a\": \"b\", \"c\": \"d\"]\n\nlet delim = try RegexDelimiter(\"[;,]\")\ntry Qs.decode(\"a=b;c=d\", options: .init(delimiter: delim))\n// [\"a\": \"b\", \"c\": \"d\"]\n```\n\n### Dot notation \u0026 “decode dots in keys”\n\n```swift\ntry Qs.decode(\"a.b=c\", options: .init(allowDots: true))\n// [\"a\": [\"b\": \"c\"]]\n\nlet r = try Qs.decode(\n    \"name%252Eobj.first=John\u0026name%252Eobj.last=Doe\",\n    options: .init(decodeDotInKeys: true)\n)\n// [\"name.obj\": [\"first\": \"John\", \"last\": \"Doe\"]]\n```\n\n_Note:_ `decodeDotInKeys` implies `allowDots`; you don’t need to set both.\n\n### Empty lists \u0026 duplicates\n\n```swift\ntry Qs.decode(\"foo[]\u0026bar=baz\", options: .init(allowEmptyLists: true))\n// [\"foo\": [], \"bar\": \"baz\"]\n\ntry Qs.decode(\"foo=bar\u0026foo=baz\")\n// [\"foo\": [\"bar\", \"baz\"]]\n\ntry Qs.decode(\"foo=bar\u0026foo=baz\", options: .init(duplicates: .first))\n// [\"foo\": \"bar\"]\n\ntry Qs.decode(\"foo=bar\u0026foo=baz\", options: .init(duplicates: .last))\n// [\"foo\": \"baz\"]\n```\n\n### Charset \u0026 sentinel\n\n```swift\ntry Qs.decode(\"a=%A7\", options: .init(charset: .isoLatin1))\n// [\"a\": \"§\"]\n\ntry Qs.decode(\n    \"utf8=%E2%9C%93\u0026a=%C3%B8\",\n    options: .init(charset: .isoLatin1, charsetSentinel: true)\n)\n// [\"a\": \"ø\"]\n\ntry Qs.decode(\n    \"utf8=%26%2310003%3B\u0026a=%F8\",\n    options: .init(charset: .utf8, charsetSentinel: true)\n)\n// [\"a\": \"ø\"]\n```\n\n### Interpret numeric entities (`\u0026#1234;`)\n\n```swift\ntry Qs.decode(\n    \"a=%26%239786%3B\",\n    options: .init(charset: .isoLatin1, interpretNumericEntities: true)\n)\n// [\"a\": \"☺\"]\n```\n\n_Heads-up:_ If you also enable `comma: true`, entity interpretation happens **after** comma processing. When you use list syntax like `a[]=...`, a comma-joined scalar stays a **single** element (e.g. `[\"1,☺\"]`) inside the list, matching the library’s tests and cross-port behavior.\n\n### Lists\n\n```swift\ntry Qs.decode(\"a[]=b\u0026a[]=c\")\n// [\"a\": [\"b\", \"c\"]]\n\ntry Qs.decode(\"a[1]=c\u0026a[0]=b\")\n// [\"a\": [\"b\", \"c\"]]\n\ntry Qs.decode(\"a[1]=b\u0026a[15]=c\")\n// [\"a\": [\"b\", \"c\"]]\n\ntry Qs.decode(\"a[]=\u0026a[]=b\")\n// [\"a\": [\"\", \"b\"]]\n```\n\nLarge indices become a map by default:\n\n```swift\nlet r = try Qs.decode(\"a[100]=b\")\n// [\"a\": [\"100\": \"b\"]]\n```\n\nDisable list parsing:\n\n```swift\nlet r = try Qs.decode(\"a[]=b\", options: .init(parseLists: false))\n// [\"a\": [\"0\": \"b\"]]\n```\n\nMix notations:\n\n```swift\nlet r = try Qs.decode(\"a[0]=b\u0026a[b]=c\")\n// [\"a\": [\"0\": \"b\", \"b\": \"c\"]]\n```\n\nComma-separated values:\n\n```swift\nlet r = try Qs.decode(\"a=b,c\", options: .init(comma: true))\n// [\"a\": [\"b\", \"c\"]]\n```\n\n---\n\n## Encoding\n\n### Basics\n\n```swift\ntry Qs.encode([\"a\": \"b\"])\n// \"a=b\"\n\ntry Qs.encode([\"a\": [\"b\": \"c\"]])\n// \"a%5Bb%5D=c\"\n```\n\nDisable URI encoding for readability:\n\n```swift\ntry Qs.encode([\"a\": [\"b\": \"c\"]], options: .init(encode: false))\n// \"a[b]=c\"\n```\n\nValues-only encoding:\n\n```swift\nlet input: [String: Any] = [\n    \"a\": \"b\",\n    \"c\": [\"d\", \"e=f\"],\n    \"f\": [[\"g\"], [\"h\"]],\n]\ntry Qs.encode(input, options: .init(encodeValuesOnly: true))\n// \"a=b\u0026c[0]=d\u0026c[1]=e%3Df\u0026f[0][0]=g\u0026f[1][0]=h\"\n```\n\nCustom encoder:\n\n```swift\nlet enc: ValueEncoder = { value, _, _ in\n    // e.g. map \"č\" → \"c\", otherwise describe\n    if let s = value as? String, s == \"č\" {\n        return \"c\"\n    }\n    return String(describing: value ?? \"\")\n}\ntry Qs.encode([\"a\": [\"b\": \"č\"]], options: .init(encoder: enc))\n// \"a[b]=c\"\n```\n\n### List formats\n\n```swift\n// indices (default when encode=false)\ntry Qs.encode([\"a\": [\"b\", \"c\"]], options: .init(encode: false))\n// \"a[0]=b\u0026a[1]=c\"\n\n// brackets\ntry Qs.encode([\"a\": [\"b\", \"c\"]], options: .init(listFormat: .brackets, encode: false))\n// \"a[]=b\u0026a[]=c\"\n\n// repeat\ntry Qs.encode([\"a\": [\"b\", \"c\"]], options: .init(listFormat: .repeatKey, encode: false))\n// \"a=b\u0026a=c\"\n\n// comma\ntry Qs.encode([\"a\": [\"b\", \"c\"]], options: .init(listFormat: .comma, encode: false))\n// \"a=b,c\"\n```\n\n_Note:_ When you select `.comma`, you can set `commaRoundTrip = true` to append `[]` for single‑element lists so they can decode back into arrays. Set `commaCompactNulls = true` to drop `NSNull`/`nil` entries before joining (e.g., `[\"one\", NSNull(), nil, \"two\"]` → `one,two`). If all entries are `NSNull`/`nil`, the key is omitted; if filtering leaves a single item and `commaRoundTrip = true`, `[]` is preserved.\n\n### Nested maps and dot notation\n\n```swift\ntry Qs.encode([\"a\": [\"b\": [\"c\": \"d\", \"e\": \"f\"]]], options: .init(encode: false))\n// \"a[b][c]=d\u0026a[b][e]=f\"\n\ntry Qs.encode([\"a\": [\"b\": [\"c\": \"d\", \"e\": \"f\"]]], options: .init(allowDots: true, encode: false))\n// \"a.b.c=d\u0026a.b.e=f\"\n```\n\nEncode dots in keys:\n\n```swift\ntry Qs.encode(\n    [\"name.obj\": [\"first\": \"John\", \"last\": \"Doe\"]],\n    options: .init(allowDots: true, encodeDotInKeys: true)\n)\n// \"name%252Eobj.first=John\u0026name%252Eobj.last=Doe\"\n```\n\nEmpty lists, nulls, and other niceties:\n\n```swift\n// Allow empty lists (order preserved with OrderedDictionary input)\ntry Qs.encode([\"foo\": [Any](), \"bar\": \"baz\"], options: .init(allowEmptyLists: true, encode: false))\n// e.g. \"foo[]\u0026bar=baz\"\n\ntry Qs.encode([\"a\": \"\"])                         // \"a=\"\ntry Qs.encode([\"a\": [Any]()])                    // \"\"\ntry Qs.encode([\"a\": [\"b\": [Any]()]])             // \"\"\ntry Qs.encode([\"a\": NSNull(), \"b\": Undefined()]) // \"a=\"\n\ntry Qs.encode([\"a\": \"b\", \"c\": \"d\"], options: .init(addQueryPrefix: true))  // \"?a=b\u0026c=d\"\ntry Qs.encode([\"a\": \"b\", \"c\": \"d\"], options: .init(delimiter: \";\"))        // \"a=b;c=d\"\n```\n\n### Dates\n\n```swift\nlet date = Date(timeIntervalSince1970: 0.007) // 7 ms since epoch\n\n// Default ISO-8601 with millisecond precision (encode=false example)\ntry Qs.encode([\"a\": date], options: .init(encode: false))\n// \"a=1970-01-01T00:00:00.007Z\"\n\n// Custom serializer (epoch millis)\ntry Qs.encode(\n    [\"a\": date],\n    options: .init(\n        dateSerializer: { d in String(Int((d.timeIntervalSince1970 * 1000.0).rounded())) },\n        encode: false\n    )\n)\n// \"a=7\"\n```\n\n### Sorting and filtering\n\n```swift\n// Sort keys\nlet sort: Sorter = { a, b in\n    let la = String(describing: a ?? \"\")\n    let lb = String(describing: b ?? \"\")\n    return la.compare(lb).rawValue // -1/0/1\n}\ntry Qs.encode([\"a\": \"c\", \"z\": \"y\", \"b\": \"f\"], options: .init(encode: false, sort: sort))\n// \"a=c\u0026b=f\u0026z=y\"\n\n// Function filter (drop/transform)\nlet date = Date(timeIntervalSince1970: 0.123) // 123 ms\nlet filter = FunctionFilter { prefix, value in\n    switch prefix {\n    case \"b\": return Undefined()\n    case \"e[f]\":\n        if let d = value as? Date {\n            return Int((d.timeIntervalSince1970 * 1000.0).rounded())\n        }\n    case \"e[g][0]\":\n        if let n = value as? NSNumber {\n            return n.intValue * 2\n        }\n        if let i = value as? Int {\n            return i * 2\n        }\n    default: break\n    }\n    return value\n}\n\nlet input: [String: Any] = [\n    \"a\": \"b\",\n    \"c\": \"d\",\n    \"e\": [\"f\": date, \"g\": [2]],\n]\ntry Qs.encode(input, options: .init(encode: false, filter: filter))\n// \"a=b\u0026c=d\u0026e[f]=123\u0026e[g][0]=4\"\n\n// Iterable filter (whitelist keys/indices)\ntry Qs.encode([\"a\": \"b\", \"c\": \"d\", \"e\": \"f\"], options: .init(encode: false, filter: IterableFilter([\"a\", \"e\"])))\n// \"a=b\u0026e=f\"\n```\n\n### RFC 3986 vs RFC 1738 (spaces)\n\n```swift\ntry Qs.encode([\"a\": \"b c\"])                                   // \"a=b%20c\" (RFC 3986 default)\ntry Qs.encode([\"a\": \"b c\"], options: .init(format: .rfc3986)) // \"a=b%20c\"\ntry Qs.encode([\"a\": \"b c\"], options: .init(format: .rfc1738)) // \"a=b+c\"\n```\n\n---\n\n## `nil`, `NSNull`, and `Undefined` (null semantics)\n\nQuery strings don’t have a native null concept, so Qs uses a few conventions to mirror “JSON-style” semantics as\nclosely as possible:\n\n- `NSNull()` – use this to represent an explicit “null-like” value.\n- `Undefined()` – a special sentinel provided by `Qs` to mean “omit this key entirely”.\n- `\"\"` (empty string) – a real, present-but-empty value.\n\n### Encoding behavior (Swift → query string)\n\n| Input value         | Default (`strictNullHandling: false`) | With `strictNullHandling: true` | With `skipNulls: true` |\n|---------------------|---------------------------------------|---------------------------------|------------------------|\n| `\"foo\"`             | `a=foo`                               | `a=foo`                         | `a=foo`                |\n| `\"\"` (empty string) | `a=`                                  | `a=`                            | `a=`                   |\n| `NSNull()`          | `a=`                                  | `a` (no `=` sign)               | (omitted)              |\n| `Undefined()`       | (omitted)                             | (omitted)                       | (omitted)              |\n\nExamples:\n\n```swift\ntry Qs.encode([\"a\": NSNull()])\n// \"a=\"\n\ntry Qs.encode([\"a\": NSNull()], options: .init(strictNullHandling: true))\n// \"a\"               // bare key, no \"=\"\n\ntry Qs.encode([\"a\": NSNull()], options: .init(skipNulls: true))\n// \"\"                // key omitted\n\ntry Qs.encode([\"a\": Undefined()])\n// \"\"                // always omitted, regardless of options\n```\n\n### Decoding behavior (query string → Swift)\n\n| Input token | Default (`strictNullHandling: false`) | With `strictNullHandling: true` |\n|-------------|---------------------------------------|---------------------------------|\n| `a=`        | `[\"a\": \"\"]`                           | `[\"a\": \"\"]`                     |\n| `a`         | `[\"a\": \"\"]`                           | `[\"a\": NSNull()]`               |\n\nExamples:\n\n```swift\ntry Qs.decode(\"a\u0026b=\")\n// [\"a\": \"\", \"b\": \"\"]\n\ntry Qs.decode(\"a\u0026b=\", options: .init(strictNullHandling: true))\n// [\"a\": NSNull(), \"b\": \"\"]\n```\n\n### How this maps to JSON libraries\n\n- In **Foundation**'s **JSONSerialization**, `NSNull` is the conventional stand-in for JSON `null`.\n  → In Qs, use `NSNull()` to mean a `null`-like value.\n- In **Codable**/**JSONEncoder**, whether missing keys are emitted or omitted often depends on how your model is\n  encoded (`encode` vs `encodeIfPresent`).\n  → In `Qs`, use `Undefined()` to _always_ omit a key from the output.\n- There is **[no native “null” in query strings]()**, so preserving a true “null round-trip” requires using:\n    - `NSNull()` on `encode` and `strictNullHandling: true` (so it renders as a bare key), and\n    - `strictNullHandling: true` on `decode` (so bare keys come back as `NSNull()`).\n\nRound-trip tip:\n\n```swift\n// Encode with a null-like value:\nlet out = try Qs.encode([\"a\": NSNull()], options: .init(strictNullHandling: true))\n// \"a\"\n\n// Decode back to NSNull:\nlet back = try Qs.decode(out, options: .init(strictNullHandling: true))\n// [\"a\": NSNull()]\n```\n\nIf you simply want to drop keys when a value is not present, prefer `Undefined()` (or `skipNulls: true` when values are\n`NSNull()`), rather than encoding `NSNull()` itself.\n\n---\n\n## API surface\n\n- `Qs.decode(_:, options:) -\u003e [String: Any]`\n- `Qs.encode(_:, options:) -\u003e String`\n- `DecodeOptions` / `EncodeOptions` – configuration knobs\n- `Duplicates` / `ListFormat` / `Format` – enums matching qs.js semantics\n- `Undefined` – sentinel used by filters to omit keys\n\n---\n\n## Ordering notes\n\n- If `options.sort != nil`, that comparator decides order.\n- If `options.sort == nil` and `options.encode == false`, key order follows **input traversal** (use `OrderedDictionary`\n  for stability).\n- Arrays always preserve input order.\n\n---\n\n## Safety tips\n\n- Keep `depth` and `parameterLimit` reasonable for untrusted inputs (defaults are sane).\n- `allowEmptyLists`, `allowSparseLists`, and `parseLists` let you tune behavior for edge cases.\n- Use `strictNullHandling` to differentiate `nil` (no `=`) from empty string (`=`).\n\n---\n\n## Bench (optional)\n\nA tiny micro‑bench harness lives in `Bench/` (separate SPM package). It’s excluded from the main library.\n\n```bash\ncd Bench\nmake profile\n```\n\n---\n\n## Objective-C\n\nAn Objective‑C bridge is included as [`QsObjC`](Sources/QsObjC) (facade + delegate-style hooks).\nSee the [QsObjC README](Sources/QsObjC/README.md) for installation, options, and examples. → [Docs](https://techouse.github.io/qs-swift/qsobjc/documentation/qsobjc/)\n\n---\n\n## Linux support\n\n**Experimental** (Swift 6.0+)\n\nOn Linux, QsSwift uses [ReerKit](https://swiftpackageindex.com/reers/ReerKit)’s [`WeakMap`](https://github.com/reers/ReerKit/blob/main/Sources/ReerKit/Utility/Weak/WeakMap.swift)\nto emulate [`NSMapTable.weakToWeakObjects()`](Sources/QsSwift/Internal/NSMapTable%2BLinux.swift) (weak keys **and** weak\nvalues) for the encoder’s cycle‑detection side‑channel. This works around CoreFoundation APIs that aren’t available in\nswift‑corelibs‑foundation on Linux.\n\n#### Caveats\n\n- Some tests that construct *self‑referential* `NSArray`/`NSDictionary` graphs are wrapped in `withKnownIssue` because\n  swift‑corelibs‑foundation can crash when creating those graphs. (Apple platforms are unaffected.)\n- CI includes an **experimental Ubuntu** job and is marked `continue-on-error` while Linux behavior stabilizes.\n\n---\n\nSpecial thanks to the authors of [qs](https://www.npmjs.com/package/qs) for JavaScript:\n\n- [Jordan Harband](https://github.com/ljharb)\n- [TJ Holowaychuk](https://github.com/visionmedia/node-querystring)\n\n---\n\n## Other ports\n\n\n| Port                       | Repository                                                  | Package                                                                                                                                                                                       |\n|----------------------------|-------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| Dart                       | [techouse/qs](https://github.com/techouse/qs)               | [![pub.dev](https://img.shields.io/pub/v/qs_dart?logo=dart\u0026label=pub.dev)](https://pub.dev/packages/qs_dart)                                                                                  |\n| Python                     | [techouse/qs_codec](https://github.com/techouse/qs_codec)   | [![PyPI](https://img.shields.io/pypi/v/qs-codec?logo=python\u0026label=PyPI)](https://pypi.org/project/qs-codec/)                                                                                  |\n| Kotlin / JVM + Android AAR | [techouse/qs-kotlin](https://github.com/techouse/qs-kotlin) | [![Maven Central](https://img.shields.io/maven-central/v/io.github.techouse/qs-kotlin?logo=kotlin\u0026label=Maven%20Central)](https://central.sonatype.com/artifact/io.github.techouse/qs-kotlin) |\n| .NET / C#                  | [techouse/qs-net](https://github.com/techouse/qs-net)       | [![NuGet](https://img.shields.io/nuget/v/QsNet?logo=dotnet\u0026label=NuGet)](https://www.nuget.org/packages/QsNet)                                                                                |\n| Node.js (original)         | [ljharb/qs](https://github.com/ljharb/qs)                   | [![npm](https://img.shields.io/npm/v/qs?logo=javascript\u0026label=npm)](https://www.npmjs.com/package/qs)                                                                                         |\n\n---\n\n## License\n\nBSD 3‑Clause © [techouse](https://github.com/techouse)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftechouse%2Fqs-swift","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftechouse%2Fqs-swift","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftechouse%2Fqs-swift/lists"}