Skip to content

A query string encoding and decoding library for Android and Kotlin/JVM. Ported from qs for JavaScript.

License

Notifications You must be signed in to change notification settings

techouse/qs-kotlin

qs-kotlin

qs-kotlin

A query string encoding and decoding library for Android and Kotlin/JVM.

Ported from qs for JavaScript.

Kotlin 2.0.21 JVM 17 Maven Central Version Android AAR Test codecov Codacy Badge GitHub GitHub Sponsors GitHub Repo stars

This repo provides:

  • qs-kotlin – the core JVM library (Jar)
  • qs-kotlin-android – a thin Android AAR wrapper that re-exports the same API

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.


Highlights

  • Nested maps and lists: foo[bar][baz]=qux{ foo: { bar: { baz: "qux" } } }
  • Multiple list formats (indices, brackets, repeat, comma)
  • Dot-notation support (a.b=c) and "."-encoding toggles
  • UTF-8 and ISO-8859-1 charsets, plus optional charset sentinel (utf8=✓)
  • Custom encoders/decoders, key sorting, filtering, and strict null handling
  • Supports LocalDateTime/Instant serialization via a pluggable serializer
  • Extensive tests (Kotest), performance-minded implementation

Installation

JVM (Jar)

Kotlin:

dependencies {
    implementation("io.github.techouse:qs-kotlin:<version>")
}

Java (Gradle Groovy DSL):

dependencies {
    implementation 'io.github.techouse:qs-kotlin:<version>'
}

Android (AAR wrapper)

Kotlin:

dependencies {
    implementation("io.github.techouse:qs-kotlin-android:<version>")
}

Java (Gradle Groovy DSL):

dependencies {
    implementation 'io.github.techouse:qs-kotlin-android:<version>'
}

The Android AAR depends on Java 17 APIs. If your app’s minSdk < 26 and you use java.time transitively, enable core library desugaring in your app:

Kotlin:

android {
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
        isCoreLibraryDesugaringEnabled = true
    }
}
dependencies {
    coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
}

Java (Gradle Groovy DSL):

android {
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_17
    targetCompatibility JavaVersion.VERSION_17
    coreLibraryDesugaringEnabled = true
  }
}
dependencies {
  coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
}

Requirements

  • Kotlin 2.0.21+
  • Java 17+
  • Android wrapper: AGP 8.7+, compileSdk 35, minSdk 25

Quick start

Kotlin:

import io.github.techouse.qskotlin.QS

// Decode
val obj: Map<String, Any?> = QS.decode("foo[bar]=baz&foo[list][]=a&foo[list][]=b")
// -> mapOf("foo" to mapOf("bar" to "baz", "list" to listOf("a", "b")))

// Encode
val qs: String = QS.encode(mapOf("foo" to mapOf("bar" to "baz")))
// -> "foo%5Bbar%5D=baz"

Java:

import io.github.techouse.qskotlin.QS;

// Decode
Map<@NotNull String, @Nullable Object> obj = QS.decode("foo[bar]=baz&foo[list][]=a&foo[list][]=b");
// -> {foo={bar=baz, list=[a, b]}}

// Encode
String qs = QS.encode(Map.of("foo", Map.of("bar", "baz")));
// -> "foo%5Bbar%5D=baz"

Usage

Simple

Kotlin:

// Decode
val decoded: Map<String, Any?> = QS.decode("a=c")
// => mapOf("a" to "c")

// Encode
val encoded: String = QS.encode(mapOf("a" to "c"))
// => "a=c"

Java:

// Decode
Map<@NotNull String, @Nullable Object> decoded = QS.decode("a=c");
// => {a=c}

// Encode
String encoded = QS.encode(Map.of("a", "c"));
// => "a=c"

Decoding

Nested maps

Kotlin:

QS.decode("foo[bar]=baz")
// => mapOf("foo" to mapOf("bar" to "baz"))

QS.decode("a%5Bb%5D=c")
// => mapOf("a" to mapOf("b" to "c"))

QS.decode("foo[bar][baz]=foobarbaz")
// => mapOf("foo" to mapOf("bar" to mapOf("baz" to "foobarbaz")))

Java:

QS.decode("foo[bar]=baz");
// => {foo={bar=baz}}

QS.decode("a%5Bb%5D=c");
// => {a={b=c}}

QS.decode("foo[bar][baz]=foobarbaz");
// => {foo={bar={baz=foobarbaz}}}

Depth (default: 5)

Beyond the configured depth, remaining bracket content is kept as literal text:

Kotlin:

QS.decode("a[b][c][d][e][f][g][h][i]=j")
// => mapOf("a" to mapOf("b" to mapOf("c" to mapOf("d" to mapOf("e" to mapOf("f" to mapOf("[g][h][i]" to "j")))))))

Java:

QS.decode("a[b][c][d][e][f][g][h][i]=j");
// => {a={b={c={d={e={f={[g][h][i]=j}}}}}}}

Override depth:

Kotlin:

QS.decode(
  "a[b][c][d][e][f][g][h][i]=j",
  DecodeOptions(depth = 1)
)
// => mapOf("a" to mapOf("b" to mapOf("[c][d][e][f][g][h][i]" to "j")))

Java:

QS.decode(
  "a[b][c][d][e][f][g][h][i]=j",
  DecodeOptions.builder()
    .depth(1)
    .build()
);
// => {a={b={[c][d][e][f][g][h][i]=j}}}

Parameter limit

Kotlin:

QS.decode(
  "a=b&c=d",
  DecodeOptions(parameterLimit = 1)
)
// => mapOf("a" to "b")

Java:

QS.decode(
  "a=b&c=d",
  DecodeOptions.builder()
    .parameterLimit(1)
    .build()
);
// => {a=b}

Ignore leading ?

Kotlin:

QS.decode(
  "?a=b&c=d",
  DecodeOptions(ignoreQueryPrefix = true)
)
// => mapOf("a" to "b", "c" to "d")

Java:

QS.decode(
  "?a=b&c=d",
  DecodeOptions.builder()
    .ignoreQueryPrefix(true)
    .build()
);
// => {a=b, c=d}

Custom delimiter (string or regex)

Kotlin:

QS.decode(
  "a=b;c=d",
  DecodeOptions(delimiter = StringDelimiter(";"))
)
// => mapOf("a" to "b", "c" to "d")

QS.decode(
  "a=b;c=d",
  DecodeOptions(delimiter = RegexDelimiter("[;,]"))
)
// => mapOf("a" to "b", "c" to "d")

Java:

QS.decode(
  "a=b;c=d",
  DecodeOptions.builder()
    .delimiter(Delimiter.SEMICOLON)
    .build()
);
// => {a=b, c=d}

QS.decode(
  "a=b;c=d",
  DecodeOptions.builder()
    .delimiter(new RegexDelimiter("[;,]"))
    .build()
);
// => {a=b, c=d}

Dot-notation and “decode dots in keys”

Kotlin:

QS.decode(
  "a.b=c",
  DecodeOptions(allowDots = true)
)
// => mapOf("a" to mapOf("b" to "c"))

QS.decode(
  "name%252Eobj.first=John&name%252Eobj.last=Doe",
  DecodeOptions(decodeDotInKeys = true)
)
// => mapOf("name.obj" to mapOf("first" to "John", "last" to "Doe"))

Java:

QS.decode(
  "a.b=c",
  DecodeOptions.builder()
    .allowDots(true)
    .build()
);
// => {a={b=c}}

QS.decode(
  "name%252Eobj.first=John&name%252Eobj.last=Doe",
  DecodeOptions.builder()
    .decodeDotInKeys(true)
    .build()
);
// => {name.obj={first=John, last=Doe}}

Empty lists

Kotlin:

QS.decode(
  "foo[]&bar=baz",
  DecodeOptions(allowEmptyLists = true)
)
// => mapOf("foo" to emptyList<String>(), "bar" to "baz")

Java:

QS.decode(
  "foo[]&bar=baz",
  DecodeOptions.builder()
    .allowEmptyLists(true)
    .build()
);
// => {foo=[], bar=baz}

Duplicates

Kotlin:

QS.decode("foo=bar&foo=baz")
// => mapOf("foo" to listOf("bar", "baz"))

QS.decode(
  "foo=bar&foo=baz",
  DecodeOptions(duplicates = Duplicates.COMBINE)
)
// => same as above

QS.decode(
  "foo=bar&foo=baz",
  DecodeOptions(duplicates = Duplicates.FIRST)
)
// => mapOf("foo" to "bar")

QS.decode(
  "foo=bar&foo=baz",
  DecodeOptions(duplicates = Duplicates.LAST)
)
// => mapOf("foo" to "baz")

Java:

QS.decode("foo=bar&foo=baz");
// => {foo=[bar, baz]}

QS.decode(
  "foo=bar&foo=baz",
  DecodeOptions.builder()
    .duplicates(Duplicates.COMBINE)
    .build()
);
// => same as above

QS.decode(
  "foo=bar&foo=baz",
  DecodeOptions.builder()
    .duplicates(Duplicates.FIRST)
    .build()
);
// => {foo=bar}

QS.decode(
  "foo=bar&foo=baz",
  DecodeOptions.builder()
    .duplicates(Duplicates.LAST)
    .build()
);
// => {foo=baz}

Charset and sentinel

Kotlin:

// latin1
QS.decode(
  "a=%A7",
  DecodeOptions(charset = StandardCharsets.ISO_8859_1)
)
// => mapOf("a" to "§")

// Sentinels
QS.decode(
  "utf8=%E2%9C%93&a=%C3%B8",
  DecodeOptions(
    charset = StandardCharsets.ISO_8859_1,
    charsetSentinel = true
  )
)
// => mapOf("a" to "ø")

QS.decode(
  "utf8=%26%2310003%3B&a=%F8",
  DecodeOptions(
    charset = StandardCharsets.UTF_8,
    charsetSentinel = true
  )
)
// => mapOf("a" to "ø")

Java:

QS.decode(
  "a=%A7",
  DecodeOptions.builder()
    .charset(StandardCharsets.ISO_8859_1)
    .build()
);
// => {a=§}

QS.decode(
  "utf8=%E2%9C%93&a=%C3%B8",
  DecodeOptions.builder()
    .charset(StandardCharsets.ISO_8859_1)
    .charsetSentinel(true)
    .build()
);
// => {a=ø}

QS.decode(
  "utf8=%26%2310003%3B&a=%F8",
  DecodeOptions.builder()
    .charset(StandardCharsets.UTF_8)
    .charsetSentinel(true)
    .build()
);
// => {a=ø}

Interpret numeric entities (&#1234;)

Kotlin:

QS.decode(
  "a=%26%239786%3B",
  DecodeOptions(
    charset = StandardCharsets.ISO_8859_1,
    interpretNumericEntities = true
  )
)
// => mapOf("a" to "☺")

Java:

QS.decode(
  "a=%26%239786%3B",
  DecodeOptions.builder()
    .charset(StandardCharsets.ISO_8859_1)
    .interpretNumericEntities(true)
    .build()
);
// => {a=☺}

Lists

Kotlin:

QS.decode("a[]=b&a[]=c")
// => mapOf("a" to listOf("b", "c"))

QS.decode("a[1]=c&a[0]=b")
// => mapOf("a" to listOf("b", "c"))

QS.decode("a[1]=b&a[15]=c")
// => mapOf("a" to listOf("b", "c"))

QS.decode("a[]=&a[]=b")
// => mapOf("a" to listOf("", "b"))

Java:

QS.decode("a[]=b&a[]=c");
// => {a=[b, c]}

QS.decode("a[1]=c&a[0]=b");
// => {a=[b, c]}

QS.decode("a[1]=b&a[15]=c");
// => {a=[b, c]}

QS.decode("a[]=&a[]=b");
// => {a=["", b]}

Large indices convert to a map by default:

Kotlin:

QS.decode("a[100]=b")
// => mapOf("a" to mapOf(100 to "b"))

Java:

QS.decode("a[100]=b");
// => {a={100=b}}

Disable list parsing:

Kotlin:

QS.decode(
  "a[]=b",
  DecodeOptions(parseLists = false)
)
// => mapOf("a" to mapOf(0 to "b"))

Java:

QS.decode(
  "a[]=b",
  DecodeOptions.builder()
    .parseLists(false)
    .build()
);
// => {a={0=b}}

Mixing notations merges into a map:

Kotlin:

QS.decode("a[0]=b&a[b]=c")
// => mapOf("a" to mapOf(0 to "b", "b" to "c"))

Java:

QS.decode("a[0]=b&a[b]=c");
// => {a={0=b, b=c}}

Comma-separated values:

Kotlin:

QS.decode(
  "a=b,c",
  DecodeOptions(comma = true)
)
// => mapOf("a" to listOf("b", "c"))

Java:

QS.decode(
  "a=b,c",
  DecodeOptions.builder()
    .comma(true)
    .build()
);
// => {a=[b, c]}

Primitive/scalar values

All values decode as strings by default:

Kotlin:

QS.decode("a=15&b=true&c=null")
// => mapOf("a" to "15", "b" to "true", "c" to "null")

Java:

QS.decode("a=15&b=true&c=null");
// => {a=15, b=true, c=null}

Encoding

Basics

Kotlin:

QS.encode(mapOf("a" to "b"))
// => "a=b"

QS.encode(mapOf("a" to mapOf("b" to "c")))
// => "a%5Bb%5D=c"

Java:

QS.encode(Map.of("a", "b"));
// => "a=b"

QS.encode(Map.of("a", Map.of("b", "c")));
// => "a%5Bb%5D=c"

Disable URI encoding for readability:

Kotlin:

QS.encode(
  mapOf("a" to mapOf("b" to "c")),
  EncodeOptions(encode = false)
)
// => "a[b]=c"

Java:

QS.encode(
  Map.of("a", Map.of("b", "c")),
  EncodeOptions.builder()
    .encode(false)
    .build()
);
// => "a[b]=c"

Values-only encoding:

Kotlin:

QS.encode(
  mapOf(
    "a" to "b",
    "c" to listOf("d", "e=f"),
    "f" to listOf(listOf("g"), listOf("h")),
  ),
  EncodeOptions(encodeValuesOnly = true)
)
// => "a=b&c[0]=d&c[1]=e%3Df&f[0][0]=g&f[1][0]=h"

Java:

Map<String, Object> map = new LinkedHashMap<>();
map.put("a", "b");
map.put("c", List.of("d", "e=f"));
map.put("f", List.of(List.of("g"), List.of("h")));

QS.encode(
  map,
  EncodeOptions.builder()
    .encodeValuesOnly(true)
    .build()
);
// => "a=b&c[0]=d&c[1]=e%3Df&f[0][0]=g&f[1][0]=h"

Custom encoder:

Kotlin:

QS.encode(
  mapOf("a" to mapOf("b" to "č")),
  EncodeOptions(
    encoder = { v, _, _ -> if (v == "č") "c" else v.toString() }
  )
)
// => "a[b]=c"   (with encode=false would be unescaped)

Java:

JValueEncoder enc = (v, cs, f) -> Objects.equals(v, "č") ? "c" : Objects.toString(v, "");

QS.encode(
  Map.of("a", Map.of("b", "č")),
  EncodeOptions.builder()
    .encoder(enc)
    .build()
);
// => "a%5Bb%5D=c"

List formats

Kotlin:

// default (indices)
QS.encode(
  mapOf("a" to listOf("b", "c")),
  EncodeOptions(encode = false)
)
// => "a[0]=b&a[1]=c"

// brackets
QS.encode(
  mapOf("a" to listOf("b", "c")),
  EncodeOptions(
    encode = false,
    listFormat = ListFormat.BRACKETS
  )
)
// => "a[]=b&a[]=c"

// repeat
QS.encode(
  mapOf("a" to listOf("b", "c")),
  EncodeOptions(
    encode = false,
    listFormat = ListFormat.REPEAT
  )
)
// => "a=b&a=c"

// comma
QS.encode(
  mapOf("a" to listOf("b", "c")),
  EncodeOptions(
    encode = false,
    listFormat = ListFormat.COMMA
  )
)
// => "a=b,c"

Java:

QS.encode(
  Map.of("a", List.of("b","c")),
  EncodeOptions.builder()
    .encode(false)
    .listFormat(ListFormat.INDICES)
    .build()
);
// => "a[0]=b&a[1]=c"

QS.encode(
  Map.of("a", List.of("b","c")),
  EncodeOptions.builder()
    .encode(false)
    .listFormat(ListFormat.BRACKETS)
    .build()
);
// => "a[]=b&a[]=c"

QS.encode(
  Map.of("a", List.of("b","c")),
  EncodeOptions.builder()
    .encode(false)
    .listFormat(ListFormat.REPEAT)
    .build()
);
// => "a=b&a=c"

QS.encode(
  Map.of("a", List.of("b","c")),
  EncodeOptions.builder()
    .encode(false)
    .listFormat(ListFormat.COMMA)
    .build()
);
// => "a=b,c"

Nested maps

Kotlin:

QS.encode(
  mapOf("a" to mapOf("b" to mapOf("c" to "d", "e" to "f"))),
  EncodeOptions(encode = false)
)
// => "a[b][c]=d&a[b][e]=f"

Java:

Map<String, Object> inner = new LinkedHashMap<>();
inner.put("c","d"); inner.put("e","f");
Map<String, Object> mid = new LinkedHashMap<>(); mid.put("b", inner);
Map<String, Object> root = new LinkedHashMap<>(); root.put("a", mid);

QS.encode(
  root,
  EncodeOptions.builder()
    .encode(false)
    .build()
);
// => "a[b][c]=d&a[b][e]=f"

Dot notation:

Kotlin:

QS.encode(
  mapOf("a" to mapOf("b" to mapOf("c" to "d", "e" to "f"))),
  EncodeOptions(
    encode = false,
    allowDots = true
  )
)
// => "a.b.c=d&a.b.e=f"

Java:

QS.encode(
  root,
  EncodeOptions.builder()
    .allowDots(true)
    .encode(false)
    .build()
);
// => "a.b.c=d&a.b.e=f"

Encode dots in keys:

Kotlin:

QS.encode(
  mapOf("name.obj" to mapOf("first" to "John", "last" to "Doe")),
  EncodeOptions(
    allowDots = true,
    encodeDotInKeys = true
  )
)
// => "name%252Eobj.first=John&name%252Eobj.last=Doe"

Java:

QS.encode(
  Map.of("name.obj", Map.of("first","John","last","Doe")),
  EncodeOptions.builder()
    .allowDots(true)
    .encodeDotInKeys(true)
    .build()
);
// => "name%252Eobj.first=John&name%252Eobj.last=Doe"

Allow empty lists:

Kotlin:

QS.encode(
  mapOf("foo" to emptyList<String>(), "bar" to "baz"),
  EncodeOptions(
    encode = false,
    allowEmptyLists = true
  )
)
// => "foo[]&bar=baz"

Java:

Map<String, Object> emptyMap = new LinkedHashMap<>();
emptyMap.put("foo", List.of()); emptyMap.put("bar", "baz");

QS.encode(
  emptyMap,
  EncodeOptions.builder()
    .allowEmptyLists(true)
    .encode(false)
    .build()
);
// => "foo[]&bar=baz"

Empty strings & nulls:

Kotlin:

QS.encode(mapOf("a" to ""))
// => "a="

Java:

QS.encode(Map.of("a", ""));
// => "a="

Return empty string for empty containers:

Kotlin:

QS.encode(mapOf("a" to emptyList<String>()))                   // => ""
QS.encode(mapOf("a" to emptyMap<String, Any>()))               // => ""
QS.encode(mapOf("a" to listOf(emptyMap<String, Any>())))       // => ""
QS.encode(mapOf("a" to mapOf("b" to emptyList<String>())))     // => ""
QS.encode(mapOf("a" to mapOf("b" to emptyMap<String, Any>()))) // => ""

Java:

QS.encode(Map.of("a", List.of()));              // => ""
QS.encode(Map.of("a", Map.of()));               // => ""
QS.encode(Map.of("a", List.of(Map.of())));      // => ""
QS.encode(Map.of("a", Map.of("b", List.of()))); // => ""
QS.encode(Map.of("a", Map.of("b", Map.of())));  // => ""

Omit Undefined:

Kotlin:

QS.encode(mapOf("a" to null, "b" to Undefined()))
// => "a="

Java:

Map<String, Object> omit = new LinkedHashMap<>();
omit.put("a", null); omit.put("b", Undefined.INSTANCE);

QS.encode(omit);
// => "a="

Add query prefix:

Kotlin:

QS.encode(
  mapOf("a" to "b", "c" to "d"),
  EncodeOptions(addQueryPrefix = true)
)
// => "?a=b&c=d"

Java:

QS.encode(
  Map.of("a","b","c","d"),
  EncodeOptions.builder()
    .addQueryPrefix(true)
    .build()
);
// => "?a=b&c=d"

Custom delimiter:

Kotlin:

QS.encode(
  mapOf("a" to "b", "c" to "d"),
  EncodeOptions(delimiter = Delimiter.SEMICOLON)
)
// => "a=b;c=d"

Java:

QS.encode(
  Map.of("a","b","c","d"),
  EncodeOptions.builder()
    .delimiter(Delimiter.SEMICOLON)
    .build()
);
// => "a=b;c=d"

Dates

By default, LocalDateTime is serialized using toString().

Kotlin:

val date = java.time.LocalDateTime.ofInstant(java.time.Instant.ofEpochMilli(7), java.time.ZoneId.systemDefault())

QS.encode(mapOf("a" to date), EncodeOptions(encode = false))
// => "a=1970-01-01T01:00:00.007" (example output depends on system zone)

QS.encode(
  mapOf("a" to date),
  EncodeOptions(
    encode = false,
    dateSerializer = { d -> d.atZone(java.time.ZoneId.systemDefault()).toInstant().toEpochMilli().toString() }
  )
)
// => "a=7"

Java:

var date = java.time.LocalDateTime.ofInstant(java.time.Instant.ofEpochMilli(7), java.time.ZoneId.systemDefault());

QS.encode(
  Map.of("a", date),
  EncodeOptions.builder()
    .encode(false)
    .build()
);
// => "a=1970-01-01T01:00:00.007" (example output depends on system zone)

QS.encode(
  Map.of("a", date),
  EncodeOptions.builder()
    .encode(false)
    .dateSerializer(d -> Long.toString(d.atZone(java.time.ZoneId.systemDefault()).toInstant().toEpochMilli()))
    .build()
);
// => "a=7"

Sorting & filtering

Kotlin:

// Sort keys
QS.encode(
  mapOf("a" to "c", "z" to "y", "b" to "f"),
  EncodeOptions(
    encode = false,
    sort = { a, b -> a.toString().compareTo(b.toString()) }
  )
)
// => "a=c&b=f&z=y"

// Filter by function (drop/transform values)
QS.encode(
  mapOf("a" to "b", "c" to "d", "e" to mapOf("f" to java.time.Instant.ofEpochMilli(123), "g" to listOf(2))),
  EncodeOptions(
    encode = false,
    filter = FunctionFilter { prefix, value ->
      when (prefix) {
        "b" -> Undefined()
        "e[f]" -> (value as java.time.Instant).toEpochMilli()
        "e[g][0]" -> (value as Number).toInt() * 2
        else -> value
      }
    }
  )
)
// => "a=b&c=d&e[f]=123&e[g][0]=4"

// Filter by explicit list of keys/indices
QS.encode(
  mapOf("a" to "b", "c" to "d", "e" to "f"),
  EncodeOptions(
    encode = false,
    filter = IterableFilter(listOf("a", "e"))
  )
)
// => "a=b&e=f"

QS.encode(
  mapOf("a" to listOf("b", "c", "d"), "e" to "f"),
  EncodeOptions(
    encode = false,
    filter = IterableFilter(listOf("a", 0, 2))
  )
)
// => "a[0]=b&a[2]=d"

Java:

// Sort keys
QS.encode(
  Map.of("a","c","z","y","b","f"),
  EncodeOptions.builder()
    .encode(false)
    .sort(Comparator.comparing(o -> o.toString()))
    .build()
);
// => "a=c&b=f&z=y"

// Function filter
Map<String, Object> input = new LinkedHashMap<>();
input.put("a","b"); input.put("c","d");

Map<String, Object> eMap = new LinkedHashMap<>();
eMap.put("f", java.time.Instant.ofEpochMilli(123));
eMap.put("g", List.of(2));
input.put("e", eMap);

FunctionFilter fn = FunctionFilter.from((k,v) -> switch(k) {
    case "b" -> Undefined.INSTANCE;
    case "e[f]" -> ((java.time.Instant)v).toEpochMilli();
    case "e[g][0]" -> ((Number)v).intValue()*2; 
    default -> v;
});

QS.encode(
  input,
  EncodeOptions.builder()
    .encode(false)
    .filter(fn)
    .build()
);
// => "a=b&c=d&e[f]=123&e[g][0]=4"

// Iterable filters
QS.encode(
  Map.of("a","b","c","d","e","f"),
  EncodeOptions.builder()
    .encode(false)
    .filter(new IterableFilter(List.of("a","e")))
    .build()
);
// => "a=b&e=f"

QS.encode(
  Map.of("a", List.of("b","c","d"), "e","f"),
  EncodeOptions.builder()
    .encode(false)
    .filter(new IterableFilter(List.of("a",0,2)))
    .build()
);
// => "a[0]=b&a[2]=d"

RFC 3986 vs RFC 1738 space encoding

Kotlin:

QS.encode(mapOf("a" to "b c"))
// => "a=b%20c" (RFC 3986 default)

QS.encode(
  mapOf("a" to "b c"),
  EncodeOptions(format = Format.RFC3986)
)
// => "a=b%20c"

QS.encode(
  mapOf("a" to "b c"),
  EncodeOptions(format = Format.RFC1738)
)
// => "a=b+c"

Java:

QS.encode(Map.of("a","b c"));
// => "a=b%20c" (RFC 3986 default)

QS.encode(
  Map.of("a","b c"),
  EncodeOptions.builder()
    .format(Format.RFC3986)
    .build()
);
// => "a=b%20c"

QS.encode(
  Map.of("a","b c"),
  EncodeOptions.builder()
    .format(Format.RFC1738)
    .build()
);
// => "a=b+c"

Design notes

  • 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.
  • Safety: Defaults (depth, parameterLimit) help mitigate abuse in user-supplied inputs; you can loosen them when you fully trust the source.
  • Interop: Exposes knobs similar to qs (filters, sorters, custom encoders/decoders) to make migrations straightforward.

Special thanks to the authors of qs for JavaScript:


Other ports

Port Repository Package
Dart techouse/qs pub.dev
Python techouse/qs_codec PyPI
Swift / Objective-C techouse/qs-swift SPM
.NET / C# techouse/qs-net NuGet
Node.js (original) ljharb/qs npm

License

BSD 3-Clause © techouse

About

A query string encoding and decoding library for Android and Kotlin/JVM. Ported from qs for JavaScript.

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Sponsor this project

  •  

Packages

No packages published

Contributors 3

  •  
  •  
  •  

Languages