A query string encoding and decoding library for Dart.
Ported from qs for JavaScript.
- Nested maps & lists:
a[b][c]=d⇄{ 'a': { 'b': { 'c': 'd' } } }; arrays viaa[]=b&a[]=cora[0]=b&a[1]=c. - Multiple list formats:
indices(a[0]=x),brackets(a[]=x),repeat(a=x&a=y),comma(a=x,y) with optional comma round‑trip. - Dot notation: parse/encode
a.b=cas nested; options to encode or decode literal dots in keys (encodeDotInKeys/decodeDotInKeys). - Charset & sentinel: UTF‑8 (default) and Latin‑1;
charsetSentinel(utf8=✓) to auto‑detect encoding. - Hooks & controls: custom
encoder/decoder, encode keys‑only or values‑only, alternate delimiters (string orRegExp), ignore leading?. - Duplicates & empties: strategies for duplicates (
combine/first/last); allow empty list items (foo[]) withallowEmptyLists. - Null handling:
strictNullHandling,skipNulls, and clear empty‑string behavior. - Safety limits: configurable
depth,strictDepth,parameterLimit,listLimit, andparseListstoggle. - Dates & ordering: pluggable
serializeDate, stable key ordering withsort, selective output viafilter. - RFC modes: spaces as
%20(RFC 3986, default) or+(RFC 1738). - Dart‑friendly:
Uri.queryParametersQs()andUri.toStringQs()extensions for idiomatic usage in Dart/Flutter.
Add the package to your project:
dart pub add qs_dartflutter pub add qs_dartThis repo includes an optional agent skill at
skills/qs-dart/ for users who want Codex or another
compatible agent to help prepare qs_dart encoding and decoding snippets. The skill is user-facing: it focuses on Dart
and Flutter usage, option selection, query-string interoperability, and edge-case caveats.
A simple usage example:
import'package:qs_dart/qs_dart.dart';
import'package:test/test.dart';
voidmain() {
test('Simple example', () {
expect(
QS.decode('a=c'),
equals({'a':'c'}),
);
expect(
QS.encode({'a':'c'}),
equals('a=c'),
);
});
test('Uri Extension usage', () {
expect(
Uri.parse('https://test.local/example?a[b][c]=d').queryParametersQs(),
equals({
'a': {
'b': {'c':'d'}
}
}),
);
expect(
Uri.https('test.local', '/example', {'a':'1', 'b':'2'}).toStringQs(), equals('https://test.local/example?a=1&b=2'),
);
});
}Map<String, dynamic> decode(
dynamic str, [
DecodeOptions options =constDecodeOptions(),
]);[decode] allows you to create nested [Map]s within your query strings, by surrounding the name of sub-keys with
square brackets []. For example, the string 'foo[bar]=baz' converts to:
expect(
QS.decode('foo[bar]=baz'),
equals({'foo': {'bar':'baz'}}),
);URI encoded strings work too:
expect(
QS.decode('a%5Bb%5D=c'),
equals({'a': {'b':'c'}}),
);You can also nest your [Map]s, like 'foo[bar][baz]=foobarbaz':
expect(
QS.decode('foo[bar][baz]=foobarbaz'),
equals({'foo': {'bar': {'baz':'foobarbaz'}}}),
);By default, nested [Map]s are decoded up to 5 levels deep. For example, decoding
a string like 'a[b][c][d][e][f][g][h][i]=j' yields the following [Map]:
expect(
QS.decode('a[b][c][d][e][f][g][h][i]=j'),
equals({
'a': {
'b': {
'c': {
'd': {
'e': {
'f': {
'[g][h][i]':'j'
}
}
}
}
}
}
}),
);You can override this limit with [DecodeOptions.depth]:
expect(
QS.decode(
'a[b][c][d][e][f][g][h][i]=j',
constDecodeOptions(depth:1),
),
equals({
'a': {
'b': {'[c][d][e][f][g][h][i]':'j'},
},
}),
);You can configure [decode] to throw an error when parsing nested input beyond this depth using [DecodeOptions.strictDepth] (defaults to false):
expect(
() =>QS.decode(
'a[b][c][d][e][f][g][h][i]=j',
constDecodeOptions(
depth:1,
strictDepth:true,
),
),
throwsA(isA<RangeError>()),
);The depth limit helps mitigate abuse when [decode] is used to parse user input, and it is recommended to keep it a reasonably small number. [DecodeOptions.strictDepth] adds a layer of protection by throwing a [RangeError] when the limit is exceeded, allowing you to catch and handle such cases.
For similar reasons, by default [decode] parses up to 1000 parameters. Override with [DecodeOptions.parameterLimit]:
expect(
QS.decode(
'a=b&c=d',
constDecodeOptions(parameterLimit:1),
),
equals({'a':'b'}),
);To bypass the leading question mark, use [DecodeOptions.ignoreQueryPrefix]:
expect(
QS.decode(
'?a=b&c=d',
constDecodeOptions(ignoreQueryPrefix:true),
),
equals(
{'a':'b', 'c':'d'},
),
);You can provide a custom [DecodeOptions.delimiter]:
expect(
QS.decode(
'a=b;c=d',
constDecodeOptions(delimiter:';'),
),
equals({'a':'b', 'c':'d'}),
);[DecodeOptions.delimiter] can be a [RegExp] too:
expect(
QS.decode(
'a=b;c=d',
DecodeOptions(delimiter:RegExp(r'[;,]')),
),
equals({'a':'b', 'c':'d'}),
);Enable dot notation with [DecodeOptions.allowDots]:
expect(
QS.decode(
'a.b=c',
constDecodeOptions(allowDots:true),
),
equals({'a': {'b':'c'}}),
);Decode percent-encoded dots in keys with [DecodeOptions.decodeDotInKeys].
Note: it implies [DecodeOptions.allowDots], so [decode] will error if you set [DecodeOptions.decodeDotInKeys]
to true, and DecodeOptions.allowDots to false.
expect(
QS.decode(
'name%252Eobj.first=John&name%252Eobj.last=Doe',
constDecodeOptions(decodeDotInKeys:true),
),
equals({
'name.obj': {'first':'John', 'last':'Doe'}
}),
);Allow empty [List] values within a [Map] with [DecodeOptions.allowEmptyLists].
expect(
QS.decode(
'foo[]&bar=baz',
constDecodeOptions(allowEmptyLists:true),
),
equals({
'foo': [],
'bar':'baz',
}),
);Option [DecodeOptions.duplicates] can be used to change the behavior when duplicate keys are encountered.
expect(
QS.decode('foo=bar&foo=baz'),
equals({
'foo': ['bar', 'baz']
}),
);
expect(
QS.decode(
'foo=bar&foo=baz',
constDecodeOptions(duplicates:Duplicates.combine),
),
equals({
'foo': ['bar', 'baz']
}),
);
expect(
QS.decode(
'foo=bar&foo=baz',
constDecodeOptions(duplicates:Duplicates.first),
),
equals({'foo':'bar'}),
);
expect(
QS.decode(
'foo=bar&foo=baz',
constDecodeOptions(duplicates:Duplicates.last),
),
equals({'foo':'baz'}),
);Bracket notation combines regardless of [DecodeOptions.duplicates]:
expect(
QS.decode(
'a=1&a=2&b[]=1&b[]=2',
constDecodeOptions(duplicates:Duplicates.last),
),
equals({
'a':'2',
'b': ['1', '2'],
}),
);If you have to deal with legacy browsers or services, there's also support for decoding percent-encoded octets as [latin1]:
expect(
QS.decode(
'a=%A7',
constDecodeOptions(charset: latin1),
),
equals({'a':'§'}),
);Some services add an initial utf8=✓ value to forms so that old Internet Explorer versions are more likely to submit the
form as utf-8. Additionally, the server can check the value against wrong encodings of the checkmark character and detect
that a query string or application/x-www-form-urlencoded body was not sent as utf-8, eg. if the form had an
accept-charset parameter or the containing page had a different character set.
QS supports this mechanism via the [DecodeOptions.charsetSentinel] option.
If specified, the utf8 parameter will be omitted from the returned [Map].
It will be used to switch to [latin1] or [utf8] mode depending on how the checkmark is encoded.
Important: When you specify both the [DecodeOptions.charset] option and the [DecodeOptions.charsetSentinel] option,
the [DecodeOptions.charset] will be overridden when the request contains a utf8 parameter from which the actual charset
can be deduced. In that sense the [DecodeOptions.charset] will behave as the default charset rather than the authoritative
charset.
expect(
QS.decode(
'utf8=%E2%9C%93&a=%C3%B8',
constDecodeOptions(
charset: latin1,
charsetSentinel:true,
),
),
equals({'a':'ø'}),
);
expect(
QS.decode(
'utf8=%26%2310003%3B&a=%F8',
constDecodeOptions(
charset: utf8,
charsetSentinel:true,
),
),
equals({'a':'ø'}),
);If you want to decode the &#...; syntax to the actual character,
you can specify the [DecodeOptions.interpretNumericEntities] option as well:
expect(
QS.decode(
'a=%26%239786%3B',
constDecodeOptions(
charset: latin1,
interpretNumericEntities:true,
),
),
equals({'a':'☺'}),
);It also works when the charset has been detected in [DecodeOptions.charsetSentinel] mode.
[decode] can also decode [List]s using a similar [] notation:
expect(
QS.decode('a[]=b&a[]=c'),
equals({
'a': ['b', 'c']
}),
);You may specify an index as well:
expect(
QS.decode('a[1]=c&a[0]=b'),
equals({
'a': ['b', 'c']
}),
);Note that the only difference between an index in a [List] and a key in a [Map] is that the value between the brackets must be a number to create a [List]. When creating [List]s with specific indices, [decode] will compact a sparse [List] to only the existing values preserving their order:
expect(
QS.decode('a[1]=b&a[15]=c'),
equals({
'a': ['b', 'c']
}),
);Note that an empty string is also a value, and will be preserved:
expect(
QS.decode('a[]=&a[]=b'),
equals({
'a': ['', 'b']
}),
);
expect(
QS.decode('a[0]=b&a[1]=&a[2]=c'),
equals({
'a': ['b', '', 'c']
}),
);[decode] will also limit specifying indices in a [List] to a maximum index of 20.
Any [List] members with an index of greater than 20 will instead be converted to a [Map] with the index as the key.
This is needed to handle cases when someone sent, for example, a[999999999] and it will take significant time to iterate
over this huge [List].
expect(
QS.decode('a[100]=b'),
equals({
'a': {'100':'b'}
}),
);This limit can be overridden by passing an [DecodeOptions.listLimit] option:
expect(
QS.decode(
'a[0]=b',
constDecodeOptions(listLimit:0),
),
equals({
'a': {'0':'b'}
}),
);[DecodeOptions.listLimit] is a maximum element count. With the default limit of
20, a[19]=b can still become a [List], while a[20]=b falls back to a [Map].
To disable List parsing entirely, set [DecodeOptions.parseLists] to false.
expect(
QS.decode(
'a[]=b',
constDecodeOptions(parseLists:false),
),
equals({
'a': {'0':'b'}
}),
);If you mix notations, [decode] will merge the two items into a [Map]:
expect(
QS.decode('a[0]=b&a[b]=c'),
equals({
'a': {'0':'b', 'b':'c'}
}),
);When a key appears as both an object and a scalar, [DecodeOptions.strictMerge] wraps the conflict in a [List] by default:
expect(
QS.decode('a[b]=c&a=d'),
equals({
'a': [
{'b':'c'},
'd',
]
}),
);Set strictMerge: false to restore legacy scalar-key merge behavior:
expect(
QS.decode(
'a[b]=c&a=d',
constDecodeOptions(strictMerge:false),
),
equals({
'a': {'b':'c', 'd':true}
}),
);You can also create [List]s of [Map]s:
expect(
QS.decode('a[][b]=c'),
equals({
'a': [
{'b':'c'}
]
}),
);Some systems join list items with commas; [decode] can parse this when [DecodeOptions.comma] is set to true:
expect(
QS.decode(
'a=b,c',
constDecodeOptions(comma:true),
),
equals({
'a': ['b', 'c']
}),
);([decode] cannot convert nested [Map]s, such as 'a={b:1},{c:d}')
By default, all values are parsed as [String]s.
expect(
QS.decode('a=15&b=true&c=null'),
equals({
'a':'15',
'b':'true',
'c':'null',
}),
);Stringencode(
Object? object, [
EncodeOptions options =constEncodeOptions(),
]);[encode] will by default URI encode the output. [Map]s are stringified as you would expect:
expect(
QS.encode({'a':'b'}),
equals('a=b'),
);
expect(
QS.encode({'a': {'b':'c'}}),
equals('a%5Bb%5D=c'),
);This encoding can be disabled by setting the [EncodeOptions.encode] option to false:
expect(
QS.encode(
{
'a': {'b':'c'}
},
constEncodeOptions(encode:false),
),
equals('a[b]=c'),
);Note: when a value is a ByteBuffer, it is still decoded using the selected
charset even when encode is false, so the emitted value reflects the buffer
contents.
Encoding can be disabled for keys by setting the [EncodeOptions.encodeValuesOnly] option to true:
expect(
QS.encode(
{
'a':'b',
'c': ['d', 'e=f'],
'f': [
['g'],
['h']
]
},
constEncodeOptions(encodeValuesOnly:true),
),
equals('a=b&c[0]=d&c[1]=e%3Df&f[0][0]=g&f[1][0]=h'),
);You can supply a custom [Encoder] via [EncodeOptions.encoder]:
expect(
QS.encode(
{
'a': {'b':'č'}
},
EncodeOptions(
encoder: (
str, {
Encoding? charset,
Format? format,
}) =>switch (str) {
'č'=>'c',
_ => str,
},
),
),
equals('a[b]=c'),
);(Note: [EncodeOptions.encoder] is ignored when [EncodeOptions.encode] is false.)
Similar to [EncodeOptions.encoder] there is a [DecodeOptions.decoder] option for [decode] to override decoding of properties and values:
expect(
QS.decode(
'foo=123', DecodeOptions(
decoder: (String? str, {Encoding? charset}) =>num.tryParse(str ??'') ?? str,
),
),
equals({'foo':123}),
);Examples beyond this point will be shown as though the output is not URI encoded for clarity. Please note that the return values in these cases will be URI encoded during real usage.
When [List]s are encoded, they follow the [EncodeOptions.listFormat] option, which defaults to [ListFormat.indices]:
expect(
QS.encode(
{
'a': ['b', 'c', 'd']
},
constEncodeOptions(encode:false),
),
equals('a[0]=b&a[1]=c&a[2]=d'),
);You can override this by setting [EncodeOptions.indices] to false, or, more explicitly, the
[EncodeOptions.listFormat] option to [ListFormat.repeat]:
expect(
QS.encode(
{
'a': ['b', 'c', 'd']
},
constEncodeOptions(
encode:false,
indices:false,
),
),
equals('a=b&a=c&a=d'),
);You can choose the list encoding style with [EncodeOptions.listFormat]:
expect(
QS.encode(
{
'a': ['b', 'c']
},
constEncodeOptions(
encode:false,
listFormat:ListFormat.indices,
),
),
equals('a[0]=b&a[1]=c'),
);
expect(
QS.encode(
{
'a': ['b', 'c']
},
constEncodeOptions(
encode:false,
listFormat:ListFormat.brackets,
),
),
equals('a[]=b&a[]=c'),
);
expect(
QS.encode(
{
'a': ['b', 'c']
},
constEncodeOptions(
encode:false,
listFormat:ListFormat.repeat,
),
),
equals('a=b&a=c'),
);
expect(
QS.encode(
{
'a': ['b', 'c']
},
constEncodeOptions(
encode:false,
listFormat:ListFormat.comma,
),
),
equals('a=b,c'),
);Note: When using [EncodeOptions.listFormat] set to [ListFormat.comma], you can also pass the [EncodeOptions.commaRoundTrip]
option set to true or false, to append [] on single-item [List]s, so that they can round trip through a parse.
Set [EncodeOptions.commaCompactNulls] to true with the same format when you'd like to drop null entries instead of keeping empty slots (for example, [true, false, null, true] becomes true,false,true).
When [Map]s are encoded, by default they use bracket notation:
expect(
QS.encode(
{
'a': {
'b': {'c':'d', 'e':'f'}
}
},
constEncodeOptions(encode:false),
),
equals('a[b][c]=d&a[b][e]=f'),
);You may override this to use dot notation by setting the [EncodeOptions.allowDots] option to true:
expect(
QS.encode(
{
'a': {
'b': {'c':'d', 'e':'f'}
}
},
constEncodeOptions(
encode:false,
allowDots:true,
),
),
equals('a.b.c=d&a.b.e=f'),
);Encode literal dots in keys with [EncodeOptions.encodeDotInKeys] (use with [EncodeOptions.allowDots]):
expect(
QS.encode(
{
'name.obj': {'first':'John', 'last':'Doe'}
},
constEncodeOptions(
allowDots:true,
encodeDotInKeys:true,
),
),
equals('name%252Eobj.first=John&name%252Eobj.last=Doe'),
);Caveat: when both [EncodeOptions.encodeValuesOnly] and [EncodeOptions.encodeDotInKeys] are true, only dots in keys are encoded; values remain unchanged.
You may allow empty [List] values by setting the [EncodeOptions.allowEmptyLists] option to true:
expect(
QS.encode(
{
'foo': [],
'bar':'baz',
},
constEncodeOptions(
encode:false,
allowEmptyLists:true,
),
),
equals('foo[]&bar=baz'),
);Empty strings and null values will omit the value, but the equals sign (=) remains in place:
expect(
QS.encode(
{
'a':'',
},
),
equals('a='),
);Keys with no values (e.g., empty [Map]s or [List]s) produce no output:
expect(
QS.encode(
{
'a': [],
},
),
equals(''),
);
expect(
QS.encode(
{
'a': {},
},
),
equals(''),
);
expect(
QS.encode(
{
'a': [{}],
},
),
equals('')
);
expect(
QS.encode(
{
'a': {'b': []},
},
),
equals('')
);
expect(
QS.encode(
{
'a': {'b': {}},
},
),
equals('')
);The query string may optionally be prepended with a question mark:
expect(
QS.encode(
{
'a':'b',
'c':'d',
},
constEncodeOptions(addQueryPrefix:true),
),
equals('?a=b&c=d'),
);The delimiter may be overridden as well:
expect(
QS.encode(
{
'a':'b',
'c':'d',
},
constEncodeOptions(delimiter:';'),
),
equals('a=b;c=d'),
);If you only want to override the serialization of [DateTime] objects, you can provide a custom [DateSerializer] in the [EncodeOptions.serializeDate] option:
expect(
QS.encode(
{
'a':DateTime.fromMillisecondsSinceEpoch(7).toUtc(),
},
constEncodeOptions(encode:false),
),
equals('a=1970-01-01T00:00:00.007Z'),
);
expect(
QS.encode(
{
'a':DateTime.fromMillisecondsSinceEpoch(7).toUtc(),
},
EncodeOptions(
encode:false,
serializeDate: (DateTime date) =>
date.millisecondsSinceEpoch.toString(),
),
),
equals('a=7'),
);You may use the [EncodeOptions.sort] option to affect the order of parameter keys:
expect(
QS.encode(
{
'a':'c',
'z':'y',
'b':'f',
},
EncodeOptions(
encode:false,
sort: (a, b) => a.compareTo(b),
),
),
equals('a=c&b=f&z=y'),
);Finally, you can use the [EncodeOptions.filter] option to restrict which keys will be included in the encoded output. If you pass a [Function], it will be called for each key to obtain the replacement value. Otherwise, if you pass a [List], it will be used to select properties and [List] indices to be encoded:
expect(
QS.encode(
{
'a':'b',
'c':'d',
'e': {
'f':DateTime.fromMillisecondsSinceEpoch(123),
'g': [2],
},
},
EncodeOptions(
encode:false,
filter: (prefix, value) =>switch (prefix) {
'e[f]'=> (value asDateTime).millisecondsSinceEpoch,
'e[g][0]'=> (value asnum) *2,
_ => value,
},
),
),
equals('a=b&c=d&e[f]=123&e[g][0]=4'),
);
expect(
QS.encode(
{
'a':'b',
'c':'d',
'e':'f',
},
constEncodeOptions(
encode:false,
filter: ['a', 'e'],
),
),
equals('a=b&e=f'),
);
expect(
QS.encode(
{
'a': ['b', 'c', 'd'],
'e':'f',
},
constEncodeOptions(
encode:false,
filter: ['a', 0, 2],
),
),
equals('a[0]=b&a[2]=d'),
);By default, null values are treated like empty strings:
expect(
QS.encode(
{
'a':null,
'b':'',
},
),
equals('a=&b='),
);Decoding does not distinguish between parameters with and without equal signs. Both are converted to empty strings.
expect(
QS.decode('a&b='),
equals({
'a':'',
'b':'',
}),
);To distinguish between null values and empty [String]s use the [EncodeOptions.strictNullHandling] flag.
In the result string the null values have no = sign:
expect(
QS.encode(
{
'a':null,
'b':'',
},
constEncodeOptions(strictNullHandling:true),
),
equals('a&b='),
);To decode values without = back to null use the [DecodeOptions.strictNullHandling] flag:
expect(
QS.decode(
'a&b=',
constDecodeOptions(strictNullHandling:true),
),
equals({
'a':null,
'b':'',
}),
);To completely skip rendering keys with null values, use the [EncodeOptions.skipNulls] flag:
expect(
QS.encode(
{
'a':'b',
'c':null,
},
constEncodeOptions(skipNulls:true),
),
equals('a=b'),
);If you're communicating with legacy systems, you can switch to [latin1] using the [EncodeOptions.charset] option:
expect(
QS.encode(
{
'æ':'æ',
},
constEncodeOptions(charset: latin1),
),
equals('%E6=%E6'),
);Characters that don't exist in [latin1] will be converted to numeric entities, similar to what browsers do:
expect(
QS.encode(
{
'a':'☺',
},
constEncodeOptions(charset: latin1),
),
equals('a=%26%239786%3B'),
);You can use the [EncodeOptions.charsetSentinel] option to announce the character by including an utf8=✓ parameter with
the proper encoding of the checkmark, similar to what Ruby on Rails and others do when submitting forms.
expect(
QS.encode(
{
'a':'☺',
},
constEncodeOptions(charsetSentinel:true),
),
equals('utf8=%E2%9C%93&a=%E2%98%BA'),
);
expect(
QS.encode(
{
'a':'æ',
},
constEncodeOptions(
charset: latin1,
charsetSentinel:true,
),
),
equals('utf8=%26%2310003%3B&a=%E6'),
);By default, the encoding and decoding of characters is done in [utf8], and [latin1] support is also built in via the [EncodeOptions.charset] and [DecodeOptions.charset] parameter, respectively.
If you wish to encode query strings to a different character set (i.e. Shift JIS) you can use the euc package
expect(
QS.encode(
{
'a':'こんにちは!',
},
EncodeOptions(
encoder: (str, {Encoding? charset, Format? format}) {
if ((str asString?)?.isNotEmpty ??false) {
finalUint8List buf =Uint8List.fromList(
ShiftJIS().encode(str!),
);
finalList<String> result = [
for (int i =0; i < buf.length; ++i) buf[i].toRadixString(16)
];
return'%${result.join('%')}';
}
return'';
},
),
),
equals('%61=%82%b1%82%f1%82%c9%82%bf%82%cd%81%49'),
);This also works for decoding of query strings:
expect(
QS.decode(
'%61=%82%b1%82%f1%82%c9%82%bf%82%cd%81%49',
DecodeOptions(
decoder: (str, {Encoding? charset}) {
if (str ==null) {
returnnull;
}
finalRegExp reg =RegExp(r'%([0-9A-F]{2})', caseSensitive:false);
finalList<int> result = [];
Match? parts;
while ((parts = reg.firstMatch(str!)) !=null&& parts !=null) {
result.add(int.parse(parts.group(1)!, radix:16));
str = str.substring(parts.end);
}
returnShiftJIS().decode(
Uint8List.fromList(result),
);
},
),
),
equals({
'a':'こんにちは!',
}),
);The default [EncodeOptions.format] is [Format.rfc3986] which encodes ' ' to %20 which is backward compatible.
You can also set the [EncodeOptions.format] to [Format.rfc1738] which encodes ' ' to +.
expect(
QS.encode(
{
'a':'b c',
},
),
equals('a=b%20c'),
);
expect(
QS.encode(
{
'a':'b c',
},
constEncodeOptions(format:Format.rfc3986),
),
equals('a=b%20c'),
);
expect(
QS.encode(
{
'a':'b c',
},
constEncodeOptions(format:Format.rfc1738),
),
equals('a=b+c'),
);| Port | Repository | Package |
|---|---|---|
| Python | techouse/qs_codec | |
| Kotlin / JVM + Android AAR | techouse/qs-kotlin | |
| Swift / Objective-C | techouse/qs-swift | |
| .NET / C# | techouse/qs-net | |
| Rust | techouse/qs_rust | |
| Node.js (original) | ljharb/qs |
Special thanks to the authors of qs for JavaScript:
