qs-kotlin Usage Assistant
Help users parse and build query strings with the Kotlin/JVM qs-kotlin
package, the Android qs-kotlin-android wrapper, and the optional OkHttp,
Ktor, and Spring Web integration modules.
Focus on user application code and interoperability outcomes, not repository
maintenance.
Start With Inputs
Before producing a final snippet, collect only the missing details that change the code:
- Runtime: Kotlin/JVM, Android, Java,
java.net.URI, OkHttp, Ktor, Spring Web, tests, backend code, or generated example. - Direction: decode an incoming query string, encode Kotlin or Java data, or normalize query-string handling around an existing URL/request object.
- The actual query string or data structure when available.
- Target API convention for lists: indexed brackets, empty brackets, repeated keys, or comma-separated values.
- Whether the query may include a leading
?, dot notation, literal dots in keys, duplicate keys, custom delimiters, comma-separated lists,nullflags, ISO-8859-1/legacy charset behavior, Android AAR requirements, or untrusted user input.
Do not over-ask when the desired behavior is obvious. State assumptions in the answer and give the user a concrete snippet they can paste.
Installation
Use the core JVM artifact for normal Kotlin/JVM and most Android projects:
dependencies {
implementation("io.github.techouse:qs-kotlin:<version>")
}
Use the Android AAR wrapper when the project specifically wants an AAR coordinate and Android Gradle Plugin metadata:
dependencies {
implementation("io.github.techouse:qs-kotlin-android:<version>")
}
Add only the optional integration artifact for the framework URL helper the application uses. Each integration module depends on the core artifact.
OkHttp:
dependencies {
implementation("io.github.techouse:qs-kotlin-okhttp:<version>")
}
Ktor:
dependencies {
implementation("io.github.techouse:qs-kotlin-ktor:<version>")
}
Spring Web:
dependencies {
implementation("io.github.techouse:qs-kotlin-spring-web:<version>")
}
The library targets Java 17. Android apps with minSdk < 26 that use
java.time transitively may need core library desugaring in the app module.
Public API
Prefer package-level functions in Kotlin:
import io.github.techouse.qskotlin.decode
import io.github.techouse.qskotlin.encode
import io.github.techouse.qskotlin.models.DecodeOptions
import io.github.techouse.qskotlin.models.EncodeOptions
For Java, use the generated QS facade and Java-friendly builders:
import io.github.techouse.qskotlin.QS;
import io.github.techouse.qskotlin.models.DecodeOptions;
import io.github.techouse.qskotlin.models.EncodeOptions;
Kotlin extension helpers are also available:
import io.github.techouse.qskotlin.toQueryMap
import io.github.techouse.qskotlin.toQueryString
val params = "a[b]=c".toQueryMap()
val query = mapOf("a" to mapOf("b" to "c")).toQueryString()
Decode a java.net.URI through its raw query component:
import io.github.techouse.qskotlin.decodeQsQuery
import java.net.URI
val params = URI("https://example.com/?a%5Bb%5D=c").decodeQsQuery()
Use QS.decodeQsQuery(uri) from Java. This helper deliberately reads
URI.rawQuery; do not pass URI.query or URI.getQuery() to qs because those
accessors percent-decode before qs splits query pairs.
Optional framework helpers live in integration packages:
import io.github.techouse.qskotlin.okhttp.addQsQueryParameters
import io.github.techouse.qskotlin.ktor.appendQsQueryParameters
import io.github.techouse.qskotlin.ktor.parseQsQuery
import io.github.techouse.qskotlin.spring.web.queryQs
Base Patterns
Decode a query string into nested Kotlin values:
import io.github.techouse.qskotlin.decode
val params =
decode("a[b][c]=d&tags[]=kotlin&tags[]=android")
check(
params ==
mapOf(
"a" to mapOf("b" to mapOf("c" to "d")),
"tags" to listOf("kotlin", "android"),
)
)
Encode nested Kotlin values into a query string:
import io.github.techouse.qskotlin.encode
val query =
encode(
mapOf(
"a" to mapOf("b" to mapOf("c" to "d")),
"tags" to listOf("kotlin", "android"),
)
)
check(query == "a%5Bb%5D%5Bc%5D=d&tags%5B0%5D=kotlin&tags%5B1%5D=android")
Java callers should prefer builders for options:
import io.github.techouse.qskotlin.QS;
import io.github.techouse.qskotlin.enums.ListFormat;
import io.github.techouse.qskotlin.models.EncodeOptions;
import java.util.List;
import java.util.Map;
String query =
QS.encode(
Map.of("tags", List.of("kotlin", "android")),
EncodeOptions.builder().listFormat(ListFormat.REPEAT).build());
assert query.equals("tags=kotlin&tags=android");
Decode Recipes
Use these options with decode(query, DecodeOptions(...)) in Kotlin or
QS.decode(query, DecodeOptions.builder()...build()) in Java:
- Leading question mark:
ignoreQueryPrefix = true. java.net.URI: useuri.decodeQsQuery(options), which readsrawQueryand returns an empty map for absent, empty, or opaque query components.- Dot notation such as
a.b=c:allowDots = true. - Double-encoded literal dots in keys such as
name%252Eobj.first=John:decodeDotInKeys = true. - Duplicate keys:
duplicates = Duplicates.COMBINEkeeps all values as a list; useDuplicates.FIRSTorDuplicates.LASTto collapse plain duplicate keys. Bracket-list keys such asitems[]always combine. - Bracket lists: enabled by default; set
parseLists = falseto treat list syntax as map keys. - Empty list tokens such as
foo[]:allowEmptyLists = true. - List limits: default
listLimitis20; indices greater than or equal to the limit become map keys. Duplicate keys, mixed notation, and flat comma values contribute cumulatively. Exact-limit results remain lists; soft overflow preserves every value in a numeric-keyed map, whilethrowOnLimitExceeded = truethrows. A negative limit always overflows or throws. - Comma-separated values such as
a=b,c:comma = true. - Object/scalar conflicts such as
a[b]=c&a=d:strictMerge = truewraps the conflict in a list; setstrictMerge = falsefor legacy key-as-true behavior. - Tokens without
=asnull:strictNullHandling = true. - Custom delimiters:
delimiter = Delimiter.SEMICOLON,delimiter = StringDelimiter(";"), ordelimiter = RegexDelimiter("[;,]"). - Legacy charset input:
charset = StandardCharsets.ISO_8859_1; usecharsetSentinel = truewhen a form may includeutf8=...to signal the real charset. - HTML numeric entities:
interpretNumericEntities = true, usually with ISO-8859-1 or charset sentinel handling. - Untrusted input: keep
depth,parameterLimit, andlistLimitbounded; usestrictDepth = trueandthrowOnLimitExceeded = truewhen callers need hard failures instead of soft limiting.
Example for a request query:
import io.github.techouse.qskotlin.decode
import io.github.techouse.qskotlin.enums.Duplicates
import io.github.techouse.qskotlin.models.DecodeOptions
val params =
decode(
"?filter.status=open&tag=kotlin&tag=android",
DecodeOptions(
ignoreQueryPrefix = true,
allowDots = true,
duplicates = Duplicates.COMBINE,
),
)
check(params == mapOf("filter" to mapOf("status" to "open"), "tag" to listOf("kotlin", "android")))
Encode Recipes
Use these options with encode(data, EncodeOptions(...)) in Kotlin or
QS.encode(data, EncodeOptions.builder()...build()) in Java:
- List style defaults to
ListFormat.INDICES:tags%5B0%5D=kotlin&tags%5B1%5D=android. - Empty brackets:
listFormat = ListFormat.BRACKETS. - Repeated keys:
listFormat = ListFormat.REPEAT. - Comma-separated values:
listFormat = ListFormat.COMMA. - Single-item comma lists that must round-trip as lists:
commaRoundTrip = true. - Drop
nullitems before comma-joining lists:commaCompactNulls = true. - Dot notation for nested maps:
allowDots = true. - Literal dots in keys:
encodeDotInKeys = true; setallowDots = truewhen nested paths should use dot notation. - Add a leading
?:addQueryPrefix = true. - Custom pair delimiter:
delimiter = Delimiter.SEMICOLONordelimiter = StringDelimiter(";"). - Preserve readable bracket/dot keys while encoding values:
encodeValuesOnly = true. - Disable percent encoding entirely for debugging or documented examples:
encode = false. - Emit
nullwithout=:strictNullHandling = true. - Omit
nullkeys:skipNulls = true. - Emit empty lists as
foo[]:allowEmptyLists = true. - Omit selected values: use
skipNulls = truefornullvalues, returnnullfrom aFunctionFilter, or remove those entries before callingencode. - Legacy form spaces as
+:format = Format.RFC1738; the default isFormat.RFC3986, which emits spaces as%20. - Legacy charset output:
charset = StandardCharsets.ISO_8859_1; usecharsetSentinel = trueto prepend theutf8=...sentinel. - Custom behavior: use
encoder,dateSerializer,sort, orfilterwhen the target API needs special scalar encoding, date formatting, stable key order, or selected fields.
Example for an API that expects repeated keys:
import io.github.techouse.qskotlin.encode
import io.github.techouse.qskotlin.enums.ListFormat
import io.github.techouse.qskotlin.models.EncodeOptions
val query =
encode(
mapOf(
"q" to "query strings",
"tag" to listOf("kotlin", "android"),
),
EncodeOptions(
listFormat = ListFormat.REPEAT,
addQueryPrefix = true,
),
)
check(query == "?q=query%20strings&tag=kotlin&tag=android")
Framework Integrations
Use the framework helpers when callers need to append qs-kotlin output to URL
builder types without losing duplicate keys, name-only values, bracket notation,
or existing query parameters. Pass EncodeOptions the same way as encode.
The helpers append already encoded names and values through encoded query APIs
to avoid double-encoding %5B into %255B.
OkHttp:
import io.github.techouse.qskotlin.okhttp.addQsQueryParameters
import okhttp3.HttpUrl.Companion.toHttpUrl
val url =
"https://api.example.com/products"
.toHttpUrl()
.addQsQueryParameters(mapOf("filter" to mapOf("name" to "John")))
check(url.toString() == "https://api.example.com/products?filter%5Bname%5D=John")
Use HttpUrl.Builder.addQsQueryParameters(value, options) when the caller
already has a builder. Use HttpUrl.addQsQueryParameters(value, options) to
return a new immutable URL and leave the original unchanged.
Ktor client or shared URL building:
import io.github.techouse.qskotlin.ktor.appendQsQueryParameters
import io.ktor.http.URLBuilder
val url =
URLBuilder("https://api.example.com/products")
.appendQsQueryParameters(mapOf("tags" to listOf("a", "b")))
.build()
check(url.encodedQuery == "tags%5B0%5D=a&tags%5B1%5D=b")
Ktor server request parsing:
import io.github.techouse.qskotlin.ktor.parseQsQuery
val params = call.request.parseQsQuery()
check(params == mapOf("filter" to mapOf("name" to "John")))
parseQsQuery intentionally reads ApplicationRequest.queryString() and then
calls decode, so prefer it over decoded Ktor parameter collections when
bracket notation, duplicate keys, or original percent escapes matter.
Spring Web:
import io.github.techouse.qskotlin.spring.web.queryQs
import org.springframework.web.util.UriComponentsBuilder
val uri =
UriComponentsBuilder.fromUriString("https://api.example.com/products")
.queryQs(mapOf("filter" to mapOf("name" to "John Doe")))
.build(true)
.toUri()
check(uri.toString() == "https://api.example.com/products?filter%5Bname%5D=John%20Doe")
Always finish Spring queryQs usage with build(true).toUri() or
build(true).toUriString(). Do not use build().toUri(),
encode().build(), or build().encode() after queryQs, because Spring will
double-encode qs-kotlin's already encoded output. queryQs requires
EncodeOptions.encode = true; raw unencoded output is not safe with Spring's
encoded-component path.
There is no dedicated Retrofit integration. When full qs-kotlin fidelity is
needed, build an OkHttp HttpUrl with qs-kotlin-okhttp and pass it to
Retrofit with @Url.
Java Interop
Use builders from Java to avoid long Kotlin data-class constructors:
import io.github.techouse.qskotlin.QS;
import io.github.techouse.qskotlin.enums.Duplicates;
import io.github.techouse.qskotlin.models.DecodeOptions;
import java.util.List;
import java.util.Map;
Map<String, Object> params =
QS.decode(
"?tag=kotlin&tag=android",
DecodeOptions.builder()
.ignoreQueryPrefix(true)
.duplicates(Duplicates.COMBINE)
.build());
assert params.equals(Map.of("tag", List.of("kotlin", "android")));
Use Java-friendly functional interfaces for custom callbacks:
JDecoderfor decode customization.JValueEncoderfor encode customization.JDateSerializerforLocalDateTimeserialization.- Java
Comparatorinstances throughEncodeOptions.builder().sort(...).
Combinations To Check
Warn or adjust before giving code for these cases:
DecodeOptions(decodeDotInKeys = true, allowDots = false)is invalid.parameterLimitmust be positive, orInt.MAX_VALUEfor effectively unlimited parsing.throwOnLimitExceeded = trueturns parameter and list limit violations intoIndexOutOfBoundsException; without it, parameter parsing stops at the limit and list overflows fall back to numeric-keyed maps. Flat comma values are checked before value decoding; a comma group assigned through[]=counts as one outer list element.strictDepth = truethrows on well-formed depth overflow; with the defaultfalse, the remainder beyonddepthis kept as a trailing key segment.- Built-in charset handling supports only
StandardCharsets.UTF_8andStandardCharsets.ISO_8859_1; other encodings require a customencoderordecoder. EncodeOptions.encoderis ignored whenencode = false.- Combining
encodeValuesOnly = trueandencodeDotInKeys = trueencodes only dots in keys; values remain otherwise unchanged. DecodeOptions.commaparses simple comma-separated values, but does not decode nested map syntax such asa={b:1},{c:d}.encode(null), scalar roots, empty maps, and empty containers generally produce an empty string.- The JDK and many web frameworks flatten duplicates or nested query syntax.
Prefer
decodeorQS.decodeon the raw query string when qs-style nested or repeated values matter. - For
java.net.URI, preferdecodeQsQuery; never decodeURI.queryand then pass it to qs, because encoded delimiters and percent signs can be decoded too early. - OkHttp and Ktor URL helpers preserve qs-kotlin output by using encoded query parameter APIs; normal decoded query APIs can double-encode bracket notation.
- Ktor server code should parse
ApplicationRequest.queryString(), notqueryParameters, when qs-style nested or repeated values matter. - Spring Web
queryQsmust be followed bybuild(true)and rejectsEncodeOptions(encode = false).
Response Shape
For code-generation requests, answer with:
- A short statement of assumptions, especially language, artifact coordinate, list format, null handling, charset, prefix handling, and whether input is trusted.
- One concrete Kotlin or Java snippet using
decode,encode,toQueryMap,toQueryString,decodeQsQuery,QS, or the relevant framework helper. - A brief explanation of only the options used.
- A small verification example, such as an expected map, expected query string,
JUnit assertion, Kotest assertion, or
check(...).
Keep snippets application-oriented. Prefer public API imports from
io.github.techouse.qskotlin; do not ask users to import from
io.github.techouse.qskotlin.internal.