qs_dart 1.0.3 qs_dart: ^1.0.3 copied to clipboard
A query string encoding and decoding library for Dart. Ported from qs for JavaScript.
qs_dart #
A query string encoding and decoding library for Dart.
Ported from qs for JavaScript.
Usage #
A simple usage example:
import 'package:qs_dart/qs_dart.dart';
import 'package:test/test.dart';
void main() {
test('Simple example', () {
expect(
QS.decode('a=c'),
equals({'a': 'c'}),
);
expect(
QS.encode({'a': 'c'}),
equals('a=c'),
);
});
}
Decoding Maps #
Map decode(
dynamic str, [
DecodeOptions options = const DecodeOptions(),
]);
QS 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, when nesting Map
s QS will only decode up to 5 children deep. This means if you attempt to decode a string
like 'a[b][c][d][e][f][g][h][i]=j' your resulting Map
will be:
expect(
QS.decode('a[b][c][d][e][f][g][h][i]=j'),
equals({
'a': {
'b': {
'c': {
'd': {
'e': {
'f': {
'[g][h][i]': 'j'
}
}
}
}
}
}
}),
);
This depth can be overridden by passing a depth option to DecodeOptions.depth
:
expect(
QS.decode(
'a[b][c][d][e][f][g][h][i]=j',
const DecodeOptions(depth: 1),
),
equals({
'a': {
'b': {'[c][d][e][f][g][h][i]': 'j'},
},
}),
);
The depth limit helps mitigate abuse when QS is used to parse user input, and it is recommended to keep it a reasonably small number.
For similar reasons, by default QS will only parse up to 1000 parameters. This can be overridden by passing
a DecodeOptions.parameterLimit
option:
expect(
QS.decode(
'a=b&c=d',
const DecodeOptions(parameterLimit: 1),
),
equals({'a': 'b'}),
);
To bypass the leading question mark, use DecodeOptions.ignoreQueryPrefix
:
expect(
QS.decode(
'?a=b&c=d',
const DecodeOptions(ignoreQueryPrefix: true),
),
equals(
{'a': 'b', 'c': 'd'},
),
);
An optional DecodeOptions.delimiter
can also be passed:
expect(
QS.decode(
'a=b;c=d',
const DecodeOptions(delimiter: ';'),
),
equals({'a': 'b', 'c': 'd'}),
);
DecodeOptions.delimiter
can be a regular expression too:
expect(
QS.decode(
'a=b;c=d',
DecodeOptions(delimiter: RegExp(r'[;,]')),
),
equals({'a': 'b', 'c': 'd'}),
);
Option DecodeOptions.allowDots
can be used to enable dot notation:
expect(
QS.decode(
'a.b=c',
const DecodeOptions(allowDots: true),
),
equals({'a': {'b': 'c'}}),
);
Option DecodeOptions.decodeDotInKeys
can be used to decode dots in keys
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',
const DecodeOptions(decodeDotInKeys: true),
),
equals({
'name.obj': {'first': 'John', 'last': 'Doe'}
}),
);
Option DecodeOptions.allowEmptyLists
can be used to allowing empty list values in Map
expect(
QS.decode(
'foo[]&bar=baz',
const DecodeOptions(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',
const DecodeOptions(duplicates: Duplicates.combine),
),
equals({
'foo': ['bar', 'baz']
}),
);
expect(
QS.decode(
'foo=bar&foo=baz',
const DecodeOptions(duplicates: Duplicates.first),
),
equals({'foo': 'bar'}),
);
expect(
QS.decode(
'foo=bar&foo=baz',
const DecodeOptions(duplicates: Duplicates.last),
),
equals({'foo': 'baz'}),
);
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',
const DecodeOptions(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
/utf-8
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',
const DecodeOptions(
charset: latin1,
charsetSentinel: true,
),
),
equals({'a': 'ø'}),
);
expect(
QS.decode(
'utf8=%26%2310003%3B&a=%F8',
const DecodeOptions(
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',
const DecodeOptions(
charset: latin1,
interpretNumericEntities: true,
),
),
equals({'a': '☺'}),
);
It also works when the charset has been detected in DecodeOptions.charsetSentinel
mode.
Decoding Lists #
QS 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, QS 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']
}),
);
QS 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[1]=b',
const DecodeOptions(listLimit: 0),
),
equals({
'a': {1: 'b'}
}),
);
To disable List parsing entirely, set DecodeOptions.parseLists
to false
.
expect(
QS.decode(
'a[]=b',
const DecodeOptions(parseLists: false),
),
equals({
'a': {0: 'b'}
}),
);
If you mix notations, QS will merge the two items into a Map
:
expect(
QS.decode('a[0]=b&a[b]=c'),
equals({
'a': {0: 'b', 'b': 'c'}
}),
);
You can also create List
s of Map
s:
expect(
QS.decode('a[][b]=c'),
equals({
'a': [
{'b': 'c'}
]
}),
);
Some people use comma to join array, QS can parse it:
expect(
QS.decode(
'a=b,c',
const DecodeOptions(comma: true),
),
equals({
'a': ['b', 'c']
}),
);
(QS cannot convert nested Map
s, such as 'a={b:1},{c:d}'
)
Decoding primitive/scalar values (num
, bool
, null
, etc.) #
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',
}),
);
Encoding #
String encode(
Object? object, [
EncodeOptions options = const EncodeOptions(),
]);
When encoding, QS by default URI encodes 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'}
},
const EncodeOptions(encode: false),
),
equals('a[b]=c'),
);
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']
]
},
const EncodeOptions(encodeValuesOnly: true),
),
equals('a=b&c[0]=d&c[1]=e%3Df&f[0][0]=g&f[1][0]=h'),
);
This encoding can also be replaced by a custom Encoder
set as EncodeOptions.encoder
option:
expect(
QS.encode(
{
'a': {'b': 'č'}
},
EncodeOptions(
encoder: (
str, {
Encoding? charset,
Format? format,
}) =>
switch (str) {
'č' => 'c',
_ => str,
},
),
),
equals('a[b]=c'),
);
(Note: the EncodeOptions.encoder
option does not apply if 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']
},
const EncodeOptions(encode: false),
),
equals('a[0]=b&a[1]=c&a[2]=d'),
);
You may override this by setting the EncodeOptions.indices
option to false
, or to be more explicit, the
EncodeOptions.listFormat
option to ListFormat.repeat
:
expect(
QS.encode(
{
'a': ['b', 'c', 'd']
},
const EncodeOptions(
encode: false,
indices: false,
),
),
equals('a=b&a=c&a=d'),
);
You may use the EncodeOptions.listFormat
option to specify the format of the output List
:
expect(
QS.encode(
{
'a': ['b', 'c']
},
const EncodeOptions(
encode: false,
listFormat: ListFormat.indices,
),
),
equals('a[0]=b&a[1]=c'),
);
expect(
QS.encode(
{
'a': ['b', 'c']
},
const EncodeOptions(
encode: false,
listFormat: ListFormat.brackets,
),
),
equals('a[]=b&a[]=c'),
);
expect(
QS.encode(
{
'a': ['b', 'c']
},
const EncodeOptions(
encode: false,
listFormat: ListFormat.repeat,
),
),
equals('a=b&a=c'),
);
expect(
QS.encode(
{
'a': ['b', 'c']
},
const EncodeOptions(
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.
When Map
s are encoded, by default they use bracket notation:
expect(
QS.encode(
{
'a': {
'b': {'c': 'd', 'e': 'f'}
}
},
const EncodeOptions(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'}
}
},
const EncodeOptions(
encode: false,
allowDots: true,
),
),
equals('a.b.c=d&a.b.e=f'),
);
You may encode the dot notation in the keys of Map
with option EncodeOptions.encodeDotInKeys
by setting it to true
:
expect(
QS.encode(
{
'name.obj': {'first': 'John', 'last': 'Doe'}
},
const EncodeOptions(
allowDots: true,
encodeDotInKeys: true,
),
),
equals('name%252Eobj.first=John&name%252Eobj.last=Doe'),
);
Caveat: when EncodeOptions.encodeValuesOnly
is true
as well as EncodeOptions.encodeDotInKeys
, only dots in
keys and nothing else will be encoded.
You may allow empty List
values by setting the EncodeOptions.allowEmptyLists
option to true
:
expect(
QS.encode(
{
'foo': [],
'bar': 'baz',
},
const EncodeOptions(
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='),
);
Key with no values (such as an empty Map
or List
) will return nothing:
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('')
);
Properties that are Undefined
will be omitted entirely:
expect(
QS.encode(
{
'a': null,
'b': const Undefined(),
},
),
equals('a='),
);
The query string may optionally be prepended with a question mark:
expect(
QS.encode(
{
'a': 'b',
'c': 'd',
},
const EncodeOptions(addQueryPrefix: true),
),
equals('?a=b&c=d'),
);
The delimiter may be overridden as well:
expect(
QS.encode(
{
'a': 'b',
'c': 'd',
},
const EncodeOptions(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(),
},
const EncodeOptions(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) {
'b' => const Undefined(),
'e[f]' => (value as DateTime).millisecondsSinceEpoch,
'e[g][0]' => (value as num) * 2,
_ => value,
},
),
),
equals('a=b&c=d&e[f]=123&e[g][0]=4'),
);
expect(
QS.encode(
{
'a': 'b',
'c': 'd',
'e': 'f',
},
const EncodeOptions(
encode: false,
filter: ['a', 'e'],
),
),
equals('a=b&e=f'),
);
expect(
QS.encode(
{
'a': ['b', 'c', 'd'],
'e': 'f',
},
const EncodeOptions(
encode: false,
filter: ['a', 0, 2],
),
),
equals('a[0]=b&a[2]=d'),
);
Handling of null
values #
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': '',
},
const EncodeOptions(strictNullHandling: true),
),
equals('a&b='),
);
To decode values without =
back to null
use the DecodeOptions.strictNullHandling
flag:
expect(
QS.decode(
'a&b=',
const DecodeOptions(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,
},
const EncodeOptions(skipNulls: true),
),
equals('a=b'),
);
If you're communicating with legacy systems, you can switch to latin1
using the charset
option:
expect(
QS.encode(
{
'æ': 'æ',
},
const EncodeOptions(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': '☺',
},
const EncodeOptions(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': '☺',
},
const EncodeOptions(charsetSentinel: true),
),
equals('utf8=%E2%9C%93&a=%E2%98%BA'),
);
expect(
QS.encode(
{
'a': 'æ',
},
const EncodeOptions(
charset: latin1,
charsetSentinel: true,
),
),
equals('utf8=%26%2310003%3B&a=%E6'),
);
Dealing with special character sets #
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 as String?)?.isNotEmpty ?? false) {
final Uint8List buf = Uint8List.fromList(
ShiftJIS().encode(str!),
);
final List<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, Format? format}) {
if (str == null) {
return null;
}
final RegExp reg = RegExp(r'%([0-9A-F]{2})', caseSensitive: false);
final List<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);
}
return ShiftJIS().decode(
Uint8List.fromList(result),
);
},
),
),
equals({
'a': 'こんにちは!',
}),
);
RFC 3986 and RFC 1738 space encoding #
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',
},
const EncodeOptions(format: Format.rfc3986),
),
equals('a=b%20c'),
);
expect(
QS.encode(
{
'a': 'b c',
},
const EncodeOptions(format: Format.rfc1738),
),
equals('a=b+c'),
);
Special thanks to the authors of qs for JavaScript: