{"id":30196391,"url":"https://github.com/techouse/qs-kotlin","last_synced_at":"2026-02-15T12:02:46.061Z","repository":{"id":307726454,"uuid":"1030312392","full_name":"techouse/qs-kotlin","owner":"techouse","description":"A query string encoding and decoding library for Android and Kotlin/JVM. Ported from qs for JavaScript.","archived":false,"fork":false,"pushed_at":"2025-08-01T21:24:01.000Z","size":199,"stargazers_count":0,"open_issues_count":3,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-08-01T21:41:51.014Z","etag":null,"topics":["android","jvm","kotlin","qs","query-encoding","query-string","url-parsing","url-query"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","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":null,"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},"funding":{"github":"techouse","custom":["https://paypal.me/ktusar"]}},"created_at":"2025-08-01T12:32:29.000Z","updated_at":"2025-08-01T21:24:04.000Z","dependencies_parsed_at":"2025-08-01T21:41:54.715Z","dependency_job_id":"77a59266-ee39-41c4-a9c7-b8cf7b74d8f3","html_url":"https://github.com/techouse/qs-kotlin","commit_stats":null,"previous_names":["techouse/qs-kotlin"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/techouse/qs-kotlin","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/techouse%2Fqs-kotlin","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/techouse%2Fqs-kotlin/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/techouse%2Fqs-kotlin/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/techouse%2Fqs-kotlin/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/techouse","download_url":"https://codeload.github.com/techouse/qs-kotlin/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/techouse%2Fqs-kotlin/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":270183606,"owners_count":24541341,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-13T02:00:09.904Z","response_time":66,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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":["android","jvm","kotlin","qs","query-encoding","query-string","url-parsing","url-query"],"created_at":"2025-08-13T05:18:37.717Z","updated_at":"2026-02-08T16:07:32.208Z","avatar_url":"https://github.com/techouse.png","language":"Kotlin","funding_links":["https://github.com/sponsors/techouse","https://paypal.me/ktusar"],"categories":[],"sub_categories":[],"readme":"# qs-kotlin\n\n\u003cp align=\"center\"\u003e\n    \u003cimg src=\"https://raw.githubusercontent.com/techouse/qs-kotlin/refs/heads/main/logo.png?raw=true\" width=\"256\" alt=\"qs-kotlin\" /\u003e\n\u003c/p\u003e\n\nA query string encoding and decoding library for Android and Kotlin/JVM.\n\nPorted from [qs](https://www.npmjs.com/package/qs) for JavaScript.\n\n[![Kotlin 2.3.10](https://img.shields.io/badge/Kotlin-2.3.10-7F52FF?logo=kotlin\u0026logoColor=white)](https://github.com/JetBrains/kotlin/releases/tag/v2.3.10)\n[![JVM 17](https://img.shields.io/badge/JVM-17-007396?logo=openjdk\u0026logoColor=white)](https://openjdk.org/projects/jdk/17/)\n[![Maven Central Version](https://img.shields.io/maven-central/v/io.github.techouse/qs-kotlin)](https://central.sonatype.com/artifact/io.github.techouse/qs-kotlin)\n[![Android AAR](https://img.shields.io/badge/Android-AAR-3DDC84?logo=android\u0026logoColor=white)](https://central.sonatype.com/artifact/io.github.techouse/qs-kotlin-android)\n[![Test](https://github.com/techouse/qs-kotlin/actions/workflows/test.yml/badge.svg)](https://github.com/techouse/qs-kotlin/actions/workflows/test.yml)\n[![codecov](https://codecov.io/gh/techouse/qs-kotlin/graph/badge.svg?token=ClCDNcsxqQ)](https://codecov.io/gh/techouse/qs-kotlin)\n[![Codacy Badge](https://app.codacy.com/project/badge/Grade/4e9ab7e1fe40412bb3f7709a7d3fff23)](https://app.codacy.com/gh/techouse/qs-kotlin/dashboard?utm_source=gh\u0026utm_medium=referral\u0026utm_content=\u0026utm_campaign=Badge_grade)\n[![GitHub](https://img.shields.io/github/license/techouse/qs-kotlin)](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-kotlin)](https://github.com/techouse/qs-kotlin/stargazers)\n\nThis repo provides:\n\n- **`qs-kotlin`** – the core JVM library (Jar)\n- **`qs-kotlin-android`** – a thin Android AAR wrapper that re-exports the same API\n\n\u003e If you only target the JVM (including Android projects that are fine with a plain Jar), just use `qs-kotlin`. The Android wrapper is provided for teams that prefer an AAR coordinate and AGP metadata.\n\n---\n\n## Highlights\n\n- Nested maps and lists: `foo[bar][baz]=qux` ⇄ `{ foo: { bar: { baz: \"qux\" } } }`\n- Multiple list formats (indices, brackets, repeat, comma)\n- Dot-notation support (`a.b=c`) and `\".\"`-encoding toggles\n- UTF-8 and ISO-8859-1 charsets, plus optional charset sentinel (`utf8=✓`)\n- Custom encoders/decoders, key sorting, filtering, and strict null handling\n- Supports `LocalDateTime`/`Instant` serialization via a pluggable serializer\n- Extensive tests (Kotest), performance-minded implementation\n\n---\n\n## Installation\n\n### JVM (Jar)\n\nKotlin:\n```kotlin\ndependencies {\n    implementation(\"io.github.techouse:qs-kotlin:\u003cversion\u003e\")\n}\n```\nJava (Gradle Groovy DSL):\n```groovy\ndependencies {\n    implementation 'io.github.techouse:qs-kotlin:\u003cversion\u003e'\n}\n```\n\n### Android (AAR wrapper)\n\nKotlin:\n```kotlin\ndependencies {\n    implementation(\"io.github.techouse:qs-kotlin-android:\u003cversion\u003e\")\n}\n```\nJava (Gradle Groovy DSL):\n```groovy\ndependencies {\n    implementation 'io.github.techouse:qs-kotlin-android:\u003cversion\u003e'\n}\n```\n\n\u003e The Android AAR depends on Java 17 APIs. If your app’s `minSdk \u003c 26` and you use `java.time` transitively, enable **core library desugaring** in your app:\n\nKotlin:\n```kotlin\nandroid {\n    compileOptions {\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n        isCoreLibraryDesugaringEnabled = true\n    }\n}\ndependencies {\n    coreLibraryDesugaring(\"com.android.tools:desugar_jdk_libs:2.1.5\")\n}\n```\nJava (Gradle Groovy DSL):\n```groovy\nandroid {\n  compileOptions {\n    sourceCompatibility JavaVersion.VERSION_17\n    targetCompatibility JavaVersion.VERSION_17\n    coreLibraryDesugaringEnabled = true\n  }\n}\ndependencies {\n  coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'\n}\n```\n\n---\n\n## Requirements\n\n- Kotlin **2.3.0+**\n- Java **17+**\n- Android wrapper: AGP **8.7+**, `compileSdk 35`, `minSdk 25`\n\n---\n\n## Quick start\n\nKotlin:\n```kotlin\nimport io.github.techouse.qskotlin.QS\n\n// Decode\nval obj: Map\u003cString, Any?\u003e = QS.decode(\"foo[bar]=baz\u0026foo[list][]=a\u0026foo[list][]=b\")\n// -\u003e mapOf(\"foo\" to mapOf(\"bar\" to \"baz\", \"list\" to listOf(\"a\", \"b\")))\n\n// Encode\nval qs: String = QS.encode(mapOf(\"foo\" to mapOf(\"bar\" to \"baz\")))\n// -\u003e \"foo%5Bbar%5D=baz\"\n```\nJava:\n```java\nimport io.github.techouse.qskotlin.QS;\n\n// Decode\nMap\u003c@NotNull String, @Nullable Object\u003e obj = QS.decode(\"foo[bar]=baz\u0026foo[list][]=a\u0026foo[list][]=b\");\n// -\u003e {foo={bar=baz, list=[a, b]}}\n\n// Encode\nString qs = QS.encode(Map.of(\"foo\", Map.of(\"bar\", \"baz\")));\n// -\u003e \"foo%5Bbar%5D=baz\"\n```\n\n---\n\n## Usage\n\n### Simple\n\nKotlin:\n```kotlin\n// Decode\nval decoded: Map\u003cString, Any?\u003e = QS.decode(\"a=c\")\n// =\u003e mapOf(\"a\" to \"c\")\n\n// Encode\nval encoded: String = QS.encode(mapOf(\"a\" to \"c\"))\n// =\u003e \"a=c\"\n```\nJava:\n```java\n// Decode\nMap\u003c@NotNull String, @Nullable Object\u003e decoded = QS.decode(\"a=c\");\n// =\u003e {a=c}\n\n// Encode\nString encoded = QS.encode(Map.of(\"a\", \"c\"));\n// =\u003e \"a=c\"\n```\n\n---\n\n## Decoding\n\n### Nested maps\n\nKotlin:\n```kotlin\nQS.decode(\"foo[bar]=baz\")\n// =\u003e mapOf(\"foo\" to mapOf(\"bar\" to \"baz\"))\n\nQS.decode(\"a%5Bb%5D=c\")\n// =\u003e mapOf(\"a\" to mapOf(\"b\" to \"c\"))\n\nQS.decode(\"foo[bar][baz]=foobarbaz\")\n// =\u003e mapOf(\"foo\" to mapOf(\"bar\" to mapOf(\"baz\" to \"foobarbaz\")))\n```\nJava:\n```java\nQS.decode(\"foo[bar]=baz\");\n// =\u003e {foo={bar=baz}}\n\nQS.decode(\"a%5Bb%5D=c\");\n// =\u003e {a={b=c}}\n\nQS.decode(\"foo[bar][baz]=foobarbaz\");\n// =\u003e {foo={bar={baz=foobarbaz}}}\n```\n\n### Depth (default: 5)\n\nBeyond the configured depth, remaining bracket content is kept as literal text:\n\nKotlin:\n```kotlin\nQS.decode(\"a[b][c][d][e][f][g][h][i]=j\")\n// =\u003e mapOf(\"a\" to mapOf(\"b\" to mapOf(\"c\" to mapOf(\"d\" to mapOf(\"e\" to mapOf(\"f\" to mapOf(\"[g][h][i]\" to \"j\")))))))\n```\nJava:\n```java\nQS.decode(\"a[b][c][d][e][f][g][h][i]=j\");\n// =\u003e {a={b={c={d={e={f={[g][h][i]=j}}}}}}}\n```\n\nOverride depth:\n\nKotlin:\n```kotlin\nQS.decode(\n  \"a[b][c][d][e][f][g][h][i]=j\",\n  DecodeOptions(depth = 1)\n)\n// =\u003e mapOf(\"a\" to mapOf(\"b\" to mapOf(\"[c][d][e][f][g][h][i]\" to \"j\")))\n```\nJava:\n```java\nQS.decode(\n  \"a[b][c][d][e][f][g][h][i]=j\",\n  DecodeOptions.builder()\n    .depth(1)\n    .build()\n);\n// =\u003e {a={b={[c][d][e][f][g][h][i]=j}}}\n```\n\n### Parameter limit\n\nKotlin:\n```kotlin\nQS.decode(\n  \"a=b\u0026c=d\",\n  DecodeOptions(parameterLimit = 1)\n)\n// =\u003e mapOf(\"a\" to \"b\")\n```\nJava:\n```java\nQS.decode(\n  \"a=b\u0026c=d\",\n  DecodeOptions.builder()\n    .parameterLimit(1)\n    .build()\n);\n// =\u003e {a=b}\n```\n\n### Ignore leading `?`\n\nKotlin:\n```kotlin\nQS.decode(\n  \"?a=b\u0026c=d\",\n  DecodeOptions(ignoreQueryPrefix = true)\n)\n// =\u003e mapOf(\"a\" to \"b\", \"c\" to \"d\")\n```\nJava:\n```java\nQS.decode(\n  \"?a=b\u0026c=d\",\n  DecodeOptions.builder()\n    .ignoreQueryPrefix(true)\n    .build()\n);\n// =\u003e {a=b, c=d}\n```\n\n### Custom delimiter (string or regex)\n\nKotlin:\n```kotlin\nQS.decode(\n  \"a=b;c=d\",\n  DecodeOptions(delimiter = StringDelimiter(\";\"))\n)\n// =\u003e mapOf(\"a\" to \"b\", \"c\" to \"d\")\n\nQS.decode(\n  \"a=b;c=d\",\n  DecodeOptions(delimiter = RegexDelimiter(\"[;,]\"))\n)\n// =\u003e mapOf(\"a\" to \"b\", \"c\" to \"d\")\n```\nJava:\n```java\nQS.decode(\n  \"a=b;c=d\",\n  DecodeOptions.builder()\n    .delimiter(Delimiter.SEMICOLON)\n    .build()\n);\n// =\u003e {a=b, c=d}\n\nQS.decode(\n  \"a=b;c=d\",\n  DecodeOptions.builder()\n    .delimiter(new RegexDelimiter(\"[;,]\"))\n    .build()\n);\n// =\u003e {a=b, c=d}\n```\n\n### Dot-notation and “decode dots in keys”\n\nKotlin:\n```kotlin\nQS.decode(\n  \"a.b=c\",\n  DecodeOptions(allowDots = true)\n)\n// =\u003e mapOf(\"a\" to mapOf(\"b\" to \"c\"))\n\nQS.decode(\n  \"name%252Eobj.first=John\u0026name%252Eobj.last=Doe\",\n  DecodeOptions(decodeDotInKeys = true)\n)\n// =\u003e mapOf(\"name.obj\" to mapOf(\"first\" to \"John\", \"last\" to \"Doe\"))\n```\nJava:\n```java\nQS.decode(\n  \"a.b=c\",\n  DecodeOptions.builder()\n    .allowDots(true)\n    .build()\n);\n// =\u003e {a={b=c}}\n\nQS.decode(\n  \"name%252Eobj.first=John\u0026name%252Eobj.last=Doe\",\n  DecodeOptions.builder()\n    .decodeDotInKeys(true)\n    .build()\n);\n// =\u003e {name.obj={first=John, last=Doe}}\n```\n\n### Empty lists\n\nKotlin:\n```kotlin\nQS.decode(\n  \"foo[]\u0026bar=baz\",\n  DecodeOptions(allowEmptyLists = true)\n)\n// =\u003e mapOf(\"foo\" to emptyList\u003cString\u003e(), \"bar\" to \"baz\")\n```\nJava:\n```java\nQS.decode(\n  \"foo[]\u0026bar=baz\",\n  DecodeOptions.builder()\n    .allowEmptyLists(true)\n    .build()\n);\n// =\u003e {foo=[], bar=baz}\n```\n\n### Duplicates\n\nKotlin:\n```kotlin\nQS.decode(\"foo=bar\u0026foo=baz\")\n// =\u003e mapOf(\"foo\" to listOf(\"bar\", \"baz\"))\n\nQS.decode(\n  \"foo=bar\u0026foo=baz\",\n  DecodeOptions(duplicates = Duplicates.COMBINE)\n)\n// =\u003e same as above\n\nQS.decode(\n  \"foo=bar\u0026foo=baz\",\n  DecodeOptions(duplicates = Duplicates.FIRST)\n)\n// =\u003e mapOf(\"foo\" to \"bar\")\n\nQS.decode(\n  \"foo=bar\u0026foo=baz\",\n  DecodeOptions(duplicates = Duplicates.LAST)\n)\n// =\u003e mapOf(\"foo\" to \"baz\")\n```\nJava:\n```java\nQS.decode(\"foo=bar\u0026foo=baz\");\n// =\u003e {foo=[bar, baz]}\n\nQS.decode(\n  \"foo=bar\u0026foo=baz\",\n  DecodeOptions.builder()\n    .duplicates(Duplicates.COMBINE)\n    .build()\n);\n// =\u003e same as above\n\nQS.decode(\n  \"foo=bar\u0026foo=baz\",\n  DecodeOptions.builder()\n    .duplicates(Duplicates.FIRST)\n    .build()\n);\n// =\u003e {foo=bar}\n\nQS.decode(\n  \"foo=bar\u0026foo=baz\",\n  DecodeOptions.builder()\n    .duplicates(Duplicates.LAST)\n    .build()\n);\n// =\u003e {foo=baz}\n```\n\n### Charset and sentinel\n\nKotlin:\n```kotlin\n// latin1\nQS.decode(\n  \"a=%A7\",\n  DecodeOptions(charset = StandardCharsets.ISO_8859_1)\n)\n// =\u003e mapOf(\"a\" to \"§\")\n\n// Sentinels\nQS.decode(\n  \"utf8=%E2%9C%93\u0026a=%C3%B8\",\n  DecodeOptions(\n    charset = StandardCharsets.ISO_8859_1,\n    charsetSentinel = true\n  )\n)\n// =\u003e mapOf(\"a\" to \"ø\")\n\nQS.decode(\n  \"utf8=%26%2310003%3B\u0026a=%F8\",\n  DecodeOptions(\n    charset = StandardCharsets.UTF_8,\n    charsetSentinel = true\n  )\n)\n// =\u003e mapOf(\"a\" to \"ø\")\n```\nJava:\n```java\nQS.decode(\n  \"a=%A7\",\n  DecodeOptions.builder()\n    .charset(StandardCharsets.ISO_8859_1)\n    .build()\n);\n// =\u003e {a=§}\n\nQS.decode(\n  \"utf8=%E2%9C%93\u0026a=%C3%B8\",\n  DecodeOptions.builder()\n    .charset(StandardCharsets.ISO_8859_1)\n    .charsetSentinel(true)\n    .build()\n);\n// =\u003e {a=ø}\n\nQS.decode(\n  \"utf8=%26%2310003%3B\u0026a=%F8\",\n  DecodeOptions.builder()\n    .charset(StandardCharsets.UTF_8)\n    .charsetSentinel(true)\n    .build()\n);\n// =\u003e {a=ø}\n```\n\n### Interpret numeric entities (`\u0026#1234;`)\n\nKotlin:\n```kotlin\nQS.decode(\n  \"a=%26%239786%3B\",\n  DecodeOptions(\n    charset = StandardCharsets.ISO_8859_1,\n    interpretNumericEntities = true\n  )\n)\n// =\u003e mapOf(\"a\" to \"☺\")\n```\nJava:\n```java\nQS.decode(\n  \"a=%26%239786%3B\",\n  DecodeOptions.builder()\n    .charset(StandardCharsets.ISO_8859_1)\n    .interpretNumericEntities(true)\n    .build()\n);\n// =\u003e {a=☺}\n```\n\n### Lists\n\nKotlin:\n```kotlin\nQS.decode(\"a[]=b\u0026a[]=c\")\n// =\u003e mapOf(\"a\" to listOf(\"b\", \"c\"))\n\nQS.decode(\"a[1]=c\u0026a[0]=b\")\n// =\u003e mapOf(\"a\" to listOf(\"b\", \"c\"))\n\nQS.decode(\"a[1]=b\u0026a[15]=c\")\n// =\u003e mapOf(\"a\" to listOf(\"b\", \"c\"))\n\nQS.decode(\"a[]=\u0026a[]=b\")\n// =\u003e mapOf(\"a\" to listOf(\"\", \"b\"))\n```\nJava:\n```java\nQS.decode(\"a[]=b\u0026a[]=c\");\n// =\u003e {a=[b, c]}\n\nQS.decode(\"a[1]=c\u0026a[0]=b\");\n// =\u003e {a=[b, c]}\n\nQS.decode(\"a[1]=b\u0026a[15]=c\");\n// =\u003e {a=[b, c]}\n\nQS.decode(\"a[]=\u0026a[]=b\");\n// =\u003e {a=[\"\", b]}\n```\n\nLarge indices convert to a map by default:\n\nKotlin:\n```kotlin\nQS.decode(\"a[100]=b\")\n// =\u003e mapOf(\"a\" to mapOf(100 to \"b\"))\n```\nJava:\n```java\nQS.decode(\"a[100]=b\");\n// =\u003e {a={100=b}}\n```\n\nDisable list parsing:\n\nKotlin:\n```kotlin\nQS.decode(\n  \"a[]=b\",\n  DecodeOptions(parseLists = false)\n)\n// =\u003e mapOf(\"a\" to mapOf(0 to \"b\"))\n```\nJava:\n```java\nQS.decode(\n  \"a[]=b\",\n  DecodeOptions.builder()\n    .parseLists(false)\n    .build()\n);\n// =\u003e {a={0=b}}\n```\n\nMixing notations merges into a map:\n\nKotlin:\n```kotlin\nQS.decode(\"a[0]=b\u0026a[b]=c\")\n// =\u003e mapOf(\"a\" to mapOf(0 to \"b\", \"b\" to \"c\"))\n```\nJava:\n```java\nQS.decode(\"a[0]=b\u0026a[b]=c\");\n// =\u003e {a={0=b, b=c}}\n```\n\nComma-separated values:\n\nKotlin:\n```kotlin\nQS.decode(\n  \"a=b,c\",\n  DecodeOptions(comma = true)\n)\n// =\u003e mapOf(\"a\" to listOf(\"b\", \"c\"))\n```\nJava:\n```java\nQS.decode(\n  \"a=b,c\",\n  DecodeOptions.builder()\n    .comma(true)\n    .build()\n);\n// =\u003e {a=[b, c]}\n```\n\n### Primitive/scalar values\n\nAll values decode as strings by default:\n\nKotlin:\n```kotlin\nQS.decode(\"a=15\u0026b=true\u0026c=null\")\n// =\u003e mapOf(\"a\" to \"15\", \"b\" to \"true\", \"c\" to \"null\")\n```\nJava:\n```java\nQS.decode(\"a=15\u0026b=true\u0026c=null\");\n// =\u003e {a=15, b=true, c=null}\n```\n\n---\n\n## Encoding\n\n### Basics\n\nKotlin:\n```kotlin\nQS.encode(mapOf(\"a\" to \"b\"))\n// =\u003e \"a=b\"\n\nQS.encode(mapOf(\"a\" to mapOf(\"b\" to \"c\")))\n// =\u003e \"a%5Bb%5D=c\"\n```\nJava:\n```java\nQS.encode(Map.of(\"a\", \"b\"));\n// =\u003e \"a=b\"\n\nQS.encode(Map.of(\"a\", Map.of(\"b\", \"c\")));\n// =\u003e \"a%5Bb%5D=c\"\n```\n\nDisable URI encoding for readability:\n\nKotlin:\n```kotlin\nQS.encode(\n  mapOf(\"a\" to mapOf(\"b\" to \"c\")),\n  EncodeOptions(encode = false)\n)\n// =\u003e \"a[b]=c\"\n```\nJava:\n```java\nQS.encode(\n  Map.of(\"a\", Map.of(\"b\", \"c\")),\n  EncodeOptions.builder()\n    .encode(false)\n    .build()\n);\n// =\u003e \"a[b]=c\"\n```\n\nValues-only encoding:\n\nKotlin:\n```kotlin\nQS.encode(\n  mapOf(\n    \"a\" to \"b\",\n    \"c\" to listOf(\"d\", \"e=f\"),\n    \"f\" to listOf(listOf(\"g\"), listOf(\"h\")),\n  ),\n  EncodeOptions(encodeValuesOnly = true)\n)\n// =\u003e \"a=b\u0026c[0]=d\u0026c[1]=e%3Df\u0026f[0][0]=g\u0026f[1][0]=h\"\n```\nJava:\n```java\nMap\u003cString, Object\u003e map = new LinkedHashMap\u003c\u003e();\nmap.put(\"a\", \"b\");\nmap.put(\"c\", List.of(\"d\", \"e=f\"));\nmap.put(\"f\", List.of(List.of(\"g\"), List.of(\"h\")));\n\nQS.encode(\n  map,\n  EncodeOptions.builder()\n    .encodeValuesOnly(true)\n    .build()\n);\n// =\u003e \"a=b\u0026c[0]=d\u0026c[1]=e%3Df\u0026f[0][0]=g\u0026f[1][0]=h\"\n```\n\nCustom encoder:\n\nKotlin:\n```kotlin\nQS.encode(\n  mapOf(\"a\" to mapOf(\"b\" to \"č\")),\n  EncodeOptions(\n    encoder = { v, _, _ -\u003e if (v == \"č\") \"c\" else v.toString() }\n  )\n)\n// =\u003e \"a[b]=c\"   (with encode=false would be unescaped)\n```\nJava:\n```java\nJValueEncoder enc = (v, cs, f) -\u003e Objects.equals(v, \"č\") ? \"c\" : Objects.toString(v, \"\");\n\nQS.encode(\n  Map.of(\"a\", Map.of(\"b\", \"č\")),\n  EncodeOptions.builder()\n    .encoder(enc)\n    .build()\n);\n// =\u003e \"a%5Bb%5D=c\"\n```\n\n### List formats\n\nKotlin:\n```kotlin\n// default (indices)\nQS.encode(\n  mapOf(\"a\" to listOf(\"b\", \"c\")),\n  EncodeOptions(encode = false)\n)\n// =\u003e \"a[0]=b\u0026a[1]=c\"\n\n// brackets\nQS.encode(\n  mapOf(\"a\" to listOf(\"b\", \"c\")),\n  EncodeOptions(\n    encode = false,\n    listFormat = ListFormat.BRACKETS\n  )\n)\n// =\u003e \"a[]=b\u0026a[]=c\"\n\n// repeat\nQS.encode(\n  mapOf(\"a\" to listOf(\"b\", \"c\")),\n  EncodeOptions(\n    encode = false,\n    listFormat = ListFormat.REPEAT\n  )\n)\n// =\u003e \"a=b\u0026a=c\"\n\n// comma\nQS.encode(\n  mapOf(\"a\" to listOf(\"b\", \"c\")),\n  EncodeOptions(\n    encode = false,\n    listFormat = ListFormat.COMMA\n  )\n)\n// =\u003e \"a=b,c\"\n```\nJava:\n```java\nQS.encode(\n  Map.of(\"a\", List.of(\"b\",\"c\")),\n  EncodeOptions.builder()\n    .encode(false)\n    .listFormat(ListFormat.INDICES)\n    .build()\n);\n// =\u003e \"a[0]=b\u0026a[1]=c\"\n\nQS.encode(\n  Map.of(\"a\", List.of(\"b\",\"c\")),\n  EncodeOptions.builder()\n    .encode(false)\n    .listFormat(ListFormat.BRACKETS)\n    .build()\n);\n// =\u003e \"a[]=b\u0026a[]=c\"\n\nQS.encode(\n  Map.of(\"a\", List.of(\"b\",\"c\")),\n  EncodeOptions.builder()\n    .encode(false)\n    .listFormat(ListFormat.REPEAT)\n    .build()\n);\n// =\u003e \"a=b\u0026a=c\"\n\nQS.encode(\n  Map.of(\"a\", List.of(\"b\",\"c\")),\n  EncodeOptions.builder()\n    .encode(false)\n    .listFormat(ListFormat.COMMA)\n    .build()\n);\n// =\u003e \"a=b,c\"\n```\n\n**Note:** When `ListFormat.COMMA` is selected, you can also set `EncodeOptions.commaRoundTrip` to\n`true` or `false` to append `[]` on single-element lists so they round-trip through decoding. Set\n`EncodeOptions.commaCompactNulls` to `true` alongside the comma format when you'd like to drop\n`null` entries instead of preserving empty slots (for example, `listOf(\"one\", null, \"two\")`\nbecomes `one,two`).\n\n### Nested maps\n\nKotlin:\n```kotlin\nQS.encode(\n  mapOf(\"a\" to mapOf(\"b\" to mapOf(\"c\" to \"d\", \"e\" to \"f\"))),\n  EncodeOptions(encode = false)\n)\n// =\u003e \"a[b][c]=d\u0026a[b][e]=f\"\n```\nJava:\n```java\nMap\u003cString, Object\u003e inner = new LinkedHashMap\u003c\u003e();\ninner.put(\"c\",\"d\"); inner.put(\"e\",\"f\");\nMap\u003cString, Object\u003e mid = new LinkedHashMap\u003c\u003e(); mid.put(\"b\", inner);\nMap\u003cString, Object\u003e root = new LinkedHashMap\u003c\u003e(); root.put(\"a\", mid);\n\nQS.encode(\n  root,\n  EncodeOptions.builder()\n    .encode(false)\n    .build()\n);\n// =\u003e \"a[b][c]=d\u0026a[b][e]=f\"\n```\n\nDot notation:\n\nKotlin:\n```kotlin\nQS.encode(\n  mapOf(\"a\" to mapOf(\"b\" to mapOf(\"c\" to \"d\", \"e\" to \"f\"))),\n  EncodeOptions(\n    encode = false,\n    allowDots = true\n  )\n)\n// =\u003e \"a.b.c=d\u0026a.b.e=f\"\n```\nJava:\n```java\nQS.encode(\n  root,\n  EncodeOptions.builder()\n    .allowDots(true)\n    .encode(false)\n    .build()\n);\n// =\u003e \"a.b.c=d\u0026a.b.e=f\"\n```\n\nEncode dots in keys:\n\nKotlin:\n```kotlin\nQS.encode(\n  mapOf(\"name.obj\" to mapOf(\"first\" to \"John\", \"last\" to \"Doe\")),\n  EncodeOptions(\n    allowDots = true,\n    encodeDotInKeys = true\n  )\n)\n// =\u003e \"name%252Eobj.first=John\u0026name%252Eobj.last=Doe\"\n```\nJava:\n```java\nQS.encode(\n  Map.of(\"name.obj\", Map.of(\"first\",\"John\",\"last\",\"Doe\")),\n  EncodeOptions.builder()\n    .allowDots(true)\n    .encodeDotInKeys(true)\n    .build()\n);\n// =\u003e \"name%252Eobj.first=John\u0026name%252Eobj.last=Doe\"\n```\n\nAllow empty lists:\n\nKotlin:\n```kotlin\nQS.encode(\n  mapOf(\"foo\" to emptyList\u003cString\u003e(), \"bar\" to \"baz\"),\n  EncodeOptions(\n    encode = false,\n    allowEmptyLists = true\n  )\n)\n// =\u003e \"foo[]\u0026bar=baz\"\n```\nJava:\n```java\nMap\u003cString, Object\u003e emptyMap = new LinkedHashMap\u003c\u003e();\nemptyMap.put(\"foo\", List.of()); emptyMap.put(\"bar\", \"baz\");\n\nQS.encode(\n  emptyMap,\n  EncodeOptions.builder()\n    .allowEmptyLists(true)\n    .encode(false)\n    .build()\n);\n// =\u003e \"foo[]\u0026bar=baz\"\n```\n\nEmpty strings \u0026 nulls:\n\nKotlin:\n```kotlin\nQS.encode(mapOf(\"a\" to \"\"))\n// =\u003e \"a=\"\n```\nJava:\n```java\nQS.encode(Map.of(\"a\", \"\"));\n// =\u003e \"a=\"\n```\n\nReturn empty string for empty containers:\n\nKotlin:\n```kotlin\nQS.encode(mapOf(\"a\" to emptyList\u003cString\u003e()))                   // =\u003e \"\"\nQS.encode(mapOf(\"a\" to emptyMap\u003cString, Any\u003e()))               // =\u003e \"\"\nQS.encode(mapOf(\"a\" to listOf(emptyMap\u003cString, Any\u003e())))       // =\u003e \"\"\nQS.encode(mapOf(\"a\" to mapOf(\"b\" to emptyList\u003cString\u003e())))     // =\u003e \"\"\nQS.encode(mapOf(\"a\" to mapOf(\"b\" to emptyMap\u003cString, Any\u003e()))) // =\u003e \"\"\n```\nJava:\n```java\nQS.encode(Map.of(\"a\", List.of()));              // =\u003e \"\"\nQS.encode(Map.of(\"a\", Map.of()));               // =\u003e \"\"\nQS.encode(Map.of(\"a\", List.of(Map.of())));      // =\u003e \"\"\nQS.encode(Map.of(\"a\", Map.of(\"b\", List.of()))); // =\u003e \"\"\nQS.encode(Map.of(\"a\", Map.of(\"b\", Map.of())));  // =\u003e \"\"\n```\n\nOmit `Undefined`:\n\nKotlin:\n```kotlin\nQS.encode(mapOf(\"a\" to null, \"b\" to Undefined()))\n// =\u003e \"a=\"\n```\nJava:\n```java\nMap\u003cString, Object\u003e omit = new LinkedHashMap\u003c\u003e();\nomit.put(\"a\", null); omit.put(\"b\", Undefined.INSTANCE);\n\nQS.encode(omit);\n// =\u003e \"a=\"\n```\n\nAdd query prefix:\n\nKotlin:\n```kotlin\nQS.encode(\n  mapOf(\"a\" to \"b\", \"c\" to \"d\"),\n  EncodeOptions(addQueryPrefix = true)\n)\n// =\u003e \"?a=b\u0026c=d\"\n```\nJava:\n```java\nQS.encode(\n  Map.of(\"a\",\"b\",\"c\",\"d\"),\n  EncodeOptions.builder()\n    .addQueryPrefix(true)\n    .build()\n);\n// =\u003e \"?a=b\u0026c=d\"\n```\n\nCustom delimiter:\n\nKotlin:\n```kotlin\nQS.encode(\n  mapOf(\"a\" to \"b\", \"c\" to \"d\"),\n  EncodeOptions(delimiter = Delimiter.SEMICOLON)\n)\n// =\u003e \"a=b;c=d\"\n```\nJava:\n```java\nQS.encode(\n  Map.of(\"a\",\"b\",\"c\",\"d\"),\n  EncodeOptions.builder()\n    .delimiter(Delimiter.SEMICOLON)\n    .build()\n);\n// =\u003e \"a=b;c=d\"\n```\n\n### Dates\n\nBy default, `LocalDateTime` is serialized using `toString()`.\n\nKotlin:\n```kotlin\nval date = java.time.LocalDateTime.ofInstant(java.time.Instant.ofEpochMilli(7), java.time.ZoneId.systemDefault())\n\nQS.encode(mapOf(\"a\" to date), EncodeOptions(encode = false))\n// =\u003e \"a=1970-01-01T01:00:00.007\" (example output depends on system zone)\n\nQS.encode(\n  mapOf(\"a\" to date),\n  EncodeOptions(\n    encode = false,\n    dateSerializer = { d -\u003e d.atZone(java.time.ZoneId.systemDefault()).toInstant().toEpochMilli().toString() }\n  )\n)\n// =\u003e \"a=7\"\n```\nJava:\n```java\nvar date = java.time.LocalDateTime.ofInstant(java.time.Instant.ofEpochMilli(7), java.time.ZoneId.systemDefault());\n\nQS.encode(\n  Map.of(\"a\", date),\n  EncodeOptions.builder()\n    .encode(false)\n    .build()\n);\n// =\u003e \"a=1970-01-01T01:00:00.007\" (example output depends on system zone)\n\nQS.encode(\n  Map.of(\"a\", date),\n  EncodeOptions.builder()\n    .encode(false)\n    .dateSerializer(d -\u003e Long.toString(d.atZone(java.time.ZoneId.systemDefault()).toInstant().toEpochMilli()))\n    .build()\n);\n// =\u003e \"a=7\"\n```\n\n### Sorting \u0026 filtering\n\nKotlin:\n```kotlin\n// Sort keys\nQS.encode(\n  mapOf(\"a\" to \"c\", \"z\" to \"y\", \"b\" to \"f\"),\n  EncodeOptions(\n    encode = false,\n    sort = { a, b -\u003e a.toString().compareTo(b.toString()) }\n  )\n)\n// =\u003e \"a=c\u0026b=f\u0026z=y\"\n\n// Filter by function (drop/transform values)\nQS.encode(\n  mapOf(\"a\" to \"b\", \"c\" to \"d\", \"e\" to mapOf(\"f\" to java.time.Instant.ofEpochMilli(123), \"g\" to listOf(2))),\n  EncodeOptions(\n    encode = false,\n    filter = FunctionFilter { prefix, value -\u003e\n      when (prefix) {\n        \"b\" -\u003e Undefined()\n        \"e[f]\" -\u003e (value as java.time.Instant).toEpochMilli()\n        \"e[g][0]\" -\u003e (value as Number).toInt() * 2\n        else -\u003e value\n      }\n    }\n  )\n)\n// =\u003e \"a=b\u0026c=d\u0026e[f]=123\u0026e[g][0]=4\"\n\n// Filter by explicit list of keys/indices\nQS.encode(\n  mapOf(\"a\" to \"b\", \"c\" to \"d\", \"e\" to \"f\"),\n  EncodeOptions(\n    encode = false,\n    filter = IterableFilter(listOf(\"a\", \"e\"))\n  )\n)\n// =\u003e \"a=b\u0026e=f\"\n\nQS.encode(\n  mapOf(\"a\" to listOf(\"b\", \"c\", \"d\"), \"e\" to \"f\"),\n  EncodeOptions(\n    encode = false,\n    filter = IterableFilter(listOf(\"a\", 0, 2))\n  )\n)\n// =\u003e \"a[0]=b\u0026a[2]=d\"\n```\nJava:\n```java\n// Sort keys\nQS.encode(\n  Map.of(\"a\",\"c\",\"z\",\"y\",\"b\",\"f\"),\n  EncodeOptions.builder()\n    .encode(false)\n    .sort(Comparator.comparing(o -\u003e o.toString()))\n    .build()\n);\n// =\u003e \"a=c\u0026b=f\u0026z=y\"\n\n// Function filter\nMap\u003cString, Object\u003e input = new LinkedHashMap\u003c\u003e();\ninput.put(\"a\",\"b\"); input.put(\"c\",\"d\");\n\nMap\u003cString, Object\u003e eMap = new LinkedHashMap\u003c\u003e();\neMap.put(\"f\", java.time.Instant.ofEpochMilli(123));\neMap.put(\"g\", List.of(2));\ninput.put(\"e\", eMap);\n\nFunctionFilter fn = FunctionFilter.from((k,v) -\u003e switch(k) {\n    case \"b\" -\u003e Undefined.INSTANCE;\n    case \"e[f]\" -\u003e ((java.time.Instant)v).toEpochMilli();\n    case \"e[g][0]\" -\u003e ((Number)v).intValue()*2; \n    default -\u003e v;\n});\n\nQS.encode(\n  input,\n  EncodeOptions.builder()\n    .encode(false)\n    .filter(fn)\n    .build()\n);\n// =\u003e \"a=b\u0026c=d\u0026e[f]=123\u0026e[g][0]=4\"\n\n// Iterable filters\nQS.encode(\n  Map.of(\"a\",\"b\",\"c\",\"d\",\"e\",\"f\"),\n  EncodeOptions.builder()\n    .encode(false)\n    .filter(new IterableFilter(List.of(\"a\",\"e\")))\n    .build()\n);\n// =\u003e \"a=b\u0026e=f\"\n\nQS.encode(\n  Map.of(\"a\", List.of(\"b\",\"c\",\"d\"), \"e\",\"f\"),\n  EncodeOptions.builder()\n    .encode(false)\n    .filter(new IterableFilter(List.of(\"a\",0,2)))\n    .build()\n);\n// =\u003e \"a[0]=b\u0026a[2]=d\"\n```\n\n### RFC 3986 vs RFC 1738 space encoding\n\nKotlin:\n```kotlin\nQS.encode(mapOf(\"a\" to \"b c\"))\n// =\u003e \"a=b%20c\" (RFC 3986 default)\n\nQS.encode(\n  mapOf(\"a\" to \"b c\"),\n  EncodeOptions(format = Format.RFC3986)\n)\n// =\u003e \"a=b%20c\"\n\nQS.encode(\n  mapOf(\"a\" to \"b c\"),\n  EncodeOptions(format = Format.RFC1738)\n)\n// =\u003e \"a=b+c\"\n```\nJava:\n```java\nQS.encode(Map.of(\"a\",\"b c\"));\n// =\u003e \"a=b%20c\" (RFC 3986 default)\n\nQS.encode(\n  Map.of(\"a\",\"b c\"),\n  EncodeOptions.builder()\n    .format(Format.RFC3986)\n    .build()\n);\n// =\u003e \"a=b%20c\"\n\nQS.encode(\n  Map.of(\"a\",\"b c\"),\n  EncodeOptions.builder()\n    .format(Format.RFC1738)\n    .build()\n);\n// =\u003e \"a=b+c\"\n```\n\n---\n\n## Design notes\n\n- **Performance:** The implementation mirrors qs semantics but is optimized for Kotlin/JVM. Deep parsing, list compaction, and cycle-safe compaction are implemented iteratively where it matters.\n- **Safety:** Defaults (depth, parameterLimit) help mitigate abuse in user-supplied inputs; you can loosen them when you fully trust the source.\n- **Interop:** Exposes knobs similar to qs (filters, sorters, custom encoders/decoders) to make migrations straightforward.\n\n---\n\nSpecial thanks to the authors of [qs](https://www.npmjs.com/package/qs) for JavaScript:\n- [Jordan Harband](https://github.com/ljharb)\n- [TJ Holowaychuk](https://github.com/visionmedia/node-querystring)\n\n---\n\n## Other ports\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| Swift / Objective-C        | [techouse/qs-swift](https://github.com/techouse/qs-swift)   | [![SPM](https://img.shields.io/github/v/release/techouse/qs-swift?logo=swift\u0026label=SwiftPM)](https://swiftpackageindex.com/techouse/qs-swift) |\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-kotlin","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftechouse%2Fqs-kotlin","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftechouse%2Fqs-kotlin/lists"}