qs、query-string、querystring 和 url-parse 都是用于处理 URL 查询字符串的 JavaScript 库,但它们的侧重点和适用场景各不相同。qs 以其强大的嵌套对象解析能力著称,常用于处理复杂的表单数据。query-string 专为现代浏览器环境设计,API 简洁且对 window.location 支持友好。querystring 是 Node.js 的核心模块(也有 npm 包),但已被标记为遗留方案,功能较为基础。url-parse 则专注于解析完整的 URL 结构,查询字符串处理只是其功能的一部分,适合需要提取域名、协议等完整信息的场景。
在 Web 开发中,处理 URL 查询参数(Query Parameters)是一个看似简单实则充满陷阱的任务。不同的库对嵌套数据、编码规则和浏览器兼容性的处理方式差异巨大。本文将深入对比 qs、query-string、querystring 和 url-parse,帮助你根据实际工程需求做出正确选择。
在开始技术对比前,必须明确一点:querystring 已被官方标记为遗留(Legacy)模块。
Node.js 官方文档明确指出,querystring 模块是出于历史原因保留的,不建议在新代码中使用。npm 上的 querystring 包通常是该核心模块的 polyfill。它的主要缺陷是不支持嵌套对象解析,且编码处理不符合现代 URL 标准。
// querystring: 不支持嵌套,会变成字符串
const qs = require('querystring');
qs.parse('a[b]=c');
// 结果:{ 'a[b]': 'c' } 而不是 { a: { b: 'c' } }
建议:除非你在维护十年前的老代码,否则请避免在新项目中使用 querystring。
这四个库都能将字符串转为对象,或将对象转为字符串,但 API 设计和默认行为不同。
qs 提供了最丰富的配置选项,允许你自定义分隔符和编码方式。
// qs
import qs from 'qs';
const obj = qs.parse('a=b&c=d');
const str = qs.stringify({ a: 'b', c: 'd' });
// 输出:a=b&c=d
query-string 的 API 非常直观,专为开发者体验优化,默认自动处理很多细节。
// query-string
import queryString from 'query-string';
const obj = queryString.parse('?a=b&c=d');
const str = queryString.stringify({ a: 'b', c: 'd' });
// 输出:a=b&c=d
querystring 的 API 较为古老,需要手动去除前导问号,且功能有限。
// querystring
const querystring = require('querystring');
// 注意:通常需要先手动去掉 '?'
const obj = querystring.parse('a=b&c=d');
const str = querystring.stringify({ a: 'b', c: 'd' });
// 输出:a=b&c=d
url-parse 侧重于解析整个 URL,查询字符串只是返回对象的一个属性。
// url-parse
import Url from 'url-parse';
const url = new Url('https://example.com?a=b&c=d');
// 查询参数在 .query 属性中,通常需配合 qs 或 querystring 再次解析
const query = url.query;
// 输出:?a=b&c=d (它是字符串,不是对象)
在处理复杂表单或 API 过滤条件时,嵌套对象(如 filter[type]=admin)非常常见。这是区分库能力的关键场景。
qs 是处理嵌套数据的行业标准,能完美还原多层对象结构。
// qs
import qs from 'qs';
const input = 'user[name]=Alice&user[role]=admin';
const parsed = qs.parse(input);
// 结果:{ user: { name: 'Alice', role: 'admin' } }
const stringified = qs.stringify({ user: { name: 'Alice' } });
// 结果:user%5Bname%5D=Alice
query-string 在新版本中也支持嵌套,但行为可能需要配置,默认可能较简单。
// query-string
import queryString from 'query-string';
const input = '?user[name]=Alice&user[role]=admin';
const parsed = queryString.parse(input, { parseNumbers: false });
// 注意:默认可能不解析深层嵌套,需依赖具体版本实现
// 通常建议配合 qs 使用或确认其嵌套配置
querystring 完全不支持嵌套,会将键名视为纯字符串。
// querystring
const querystring = require('querystring');
const input = 'user[name]=Alice';
const parsed = querystring.parse(input);
// 结果:{ 'user[name]': 'Alice' }
// 无法自动转换为嵌套对象
url-parse 本身不解析查询参数内部结构,它只负责拆分 URL。
// url-parse
import Url from 'url-parse';
const url = new Url('https://example.com?user[name]=Alice');
// url.query 仅仅是字符串 "?user[name]=Alice"
// 你需要引入 qs 或 query-string 来进一步解析 url.query
在浏览器中,我们常需要直接读取或修改 window.location。不同库对此的支持程度不同。
qs 是纯逻辑库,不直接感知 window.location,需要手动传入字符串。
// qs
import qs from 'qs';
// 需要手动截取 search 部分
const params = qs.parse(window.location.search.substring(1));
query-string 提供了专门的方法直接操作 location,非常方便。
// query-string
import queryString from 'query-string';
// 直接读取当前 URL 参数
const params = queryString.parse(window.location.search);
// 直接更新 URL 而不刷新页面
const newUrl = queryString.stringifyUrl({
url: window.location.href,
query: { page: 2 }
});
window.history.pushState(null, '', newUrl);
querystring 在浏览器中通常需要打包工具(如 Webpack)进行 polyfill,原生浏览器不支持。
// querystring
// 在浏览器中直接 require 可能报错,需配置 alias
const querystring = require('querystring');
const params = querystring.parse(window.location.search.substring(1));
url-parse 设计之初就考虑了浏览器环境,能很好地处理相对路径和绝对路径。
// url-parse
import Url from 'url-parse';
// 自动处理当前页面基础 URL
const url = new Url('/path?query=1', location.href);
console.log(url.hostname);
如果你不仅需要参数,还需要域名、协议或端口信息,库的选择会受限。
qs、query-string 和 querystring 仅关注查询字符串部分,无法解析域名。
// qs / query-string / querystring
// 这些库无法告诉你主机名是什么
// 输入 'https://a.com?b=1' 会报错或忽略协议部分
url-parse 的核心优势在于此,它能将 URL 拆解为多个组成部分。
// url-parse
import Url from 'url-parse';
const url = new Url('https://user:pass@www.example.com:8080/path?query=1#hash');
console.log(url.protocol); // 'https:'
console.log(url.hostname); // 'www.example.com'
console.log(url.port); // '8080'
console.log(url.pathname); // '/path'
console.log(url.query); // '?query=1'
console.log(url.hash); // '#hash'
| 特性 | qs | query-string | querystring | url-parse |
|---|---|---|---|---|
| 嵌套对象支持 | ✅ 完美支持 | ⚠️ 部分支持/需配置 | ❌ 不支持 | ❌ 不解析内部 |
| 浏览器 Location API | ❌ 需手动处理 | ✅ 内置 helper | ❌ 需 Polyfill | ✅ 支持良好 |
| 完整 URL 解析 | ❌ 仅查询串 | ❌ 仅查询串 | ❌ 仅查询串 | ✅ 完整解析 |
| 维护状态 | ✅ 活跃 | ✅ 活跃 | ⚠️ 遗留/不推荐 | ✅ 活跃 |
| 主要用途 | 复杂数据序列化 | 前端路由参数管理 | 旧 Node 项目兼容 | URL 结构分析 |
在选择库时,请根据数据复杂度和运行环境决定:
复杂数据交互:如果你的应用涉及复杂的筛选器、嵌套表单提交,或者需要与后端严格约定参数格式(如 PHP 风格的数组),qs 是唯一可靠的选择。它的稳定性经过了时间的考验。
现代前端开发:如果你在构建 React 或 Vue 单页应用,主要需求是读取路由参数并更新 URL,query-string 能提供最简洁的开发体验。它的 stringifyUrl 方法能省去大量拼接字符串的麻烦。
URL 结构分析:如果你需要构建一个链接解析器、重定向服务或安全网关,需要提取域名和协议,url-parse 是必备工具。但请注意,提取出的 query 属性通常还需要配合 qs 进行二次解析。
避免使用:请尽量避免在新代码中引入 querystring。现代浏览器原生支持 URLSearchParams,Node.js 也有 URL 类,配合 qs 或 query-string 能获得更好的性能和安全性。
最终结论:没有绝对的“最好”,只有最适合场景的工具。对于大多数通用前端业务,query-string 提供了最佳的平衡点;而对于重型数据交互,qs 依然是后端友好的标准。
如果你的后端接口或表单数据涉及深层嵌套对象(如 a[b][c]=1),qs 是最佳选择。它在 Node.js 和浏览器中都非常稳定,配置选项丰富,允许你精确控制数组索引和编码行为。适合需要严格数据格式控制的复杂企业级应用。
选择 query-string 如果你主要在浏览器端开发,并且希望 API 尽可能简洁现代。它对 window.location.search 的操作非常直观,且默认行为符合现代 Web 标准。适合单页应用(SPA)中管理路由状态和筛选参数。
不建议在新项目中使用 querystring。它是 Node.js 的遗留模块,npm 上的包主要用于兼容旧代码。它不支持嵌套对象,且在浏览器环境中表现不如专用库。仅在维护仅依赖 Node.js 核心模块的旧系统时考虑。
当你需要解析完整的 URL 而不仅仅是查询参数时,选择 url-parse。它可以轻松提取协议、主机名、路径和查询字符串,适合构建代理服务器、爬虫或需要验证完整链接合法性的工具。适合需要处理多部分 URL 结构的中间件。
A querystring parsing and stringifying library with some added security.
Lead Maintainer: Jordan Harband
The qs module was originally created and maintained by TJ Holowaychuk.
var qs = require('qs');
var assert = require('assert');
var obj = qs.parse('a=c');
assert.deepEqual(obj, { a: 'c' });
var str = qs.stringify(obj);
assert.equal(str, 'a=c');
qs.parse(string, [options]);
qs allows you to create nested objects within your query strings, by surrounding the name of sub-keys with square brackets [].
For example, the string 'foo[bar]=baz' converts to:
assert.deepEqual(qs.parse('foo[bar]=baz'), {
foo: {
bar: 'baz'
}
});
When using the plainObjects option the parsed value is returned as a null object, created via { __proto__: null } and as such you should be aware that prototype methods will not exist on it and a user may set those names to whatever value they like:
var nullObject = qs.parse('a[hasOwnProperty]=b', { plainObjects: true });
assert.deepEqual(nullObject, { a: { hasOwnProperty: 'b' } });
By default parameters that would overwrite properties on the object prototype are ignored, if you wish to keep the data from those fields either use plainObjects as mentioned above, or set allowPrototypes to true which will allow user input to overwrite those properties.
WARNING It is generally a bad idea to enable this option as it can cause problems when attempting to use the properties that have been overwritten.
Always be careful with this option.
var protoObject = qs.parse('a[hasOwnProperty]=b', { allowPrototypes: true });
assert.deepEqual(protoObject, { a: { hasOwnProperty: 'b' } });
URI encoded strings work too:
assert.deepEqual(qs.parse('a%5Bb%5D=c'), {
a: { b: 'c' }
});
You can also nest your objects, like 'foo[bar][baz]=foobarbaz':
assert.deepEqual(qs.parse('foo[bar][baz]=foobarbaz'), {
foo: {
bar: {
baz: 'foobarbaz'
}
}
});
By default, when nesting objects qs will only parse up to 5 children deep.
This means if you attempt to parse a string like 'a[b][c][d][e][f][g][h][i]=j' your resulting object will be:
var expected = {
a: {
b: {
c: {
d: {
e: {
f: {
'[g][h][i]': 'j'
}
}
}
}
}
}
};
var string = 'a[b][c][d][e][f][g][h][i]=j';
assert.deepEqual(qs.parse(string), expected);
This depth can be overridden by passing a depth option to qs.parse(string, [options]):
var deep = qs.parse('a[b][c][d][e][f][g][h][i]=j', { depth: 1 });
assert.deepEqual(deep, { a: { b: { '[c][d][e][f][g][h][i]': 'j' } } });
You can configure qs to throw an error when parsing nested input beyond this depth using the strictDepth option (defaulted to false):
try {
qs.parse('a[b][c][d][e][f][g][h][i]=j', { depth: 1, strictDepth: true });
} catch (err) {
assert(err instanceof RangeError);
assert.strictEqual(err.message, 'Input depth exceeded depth option of 1 and strictDepth is true');
}
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. The strictDepth option adds a layer of protection by throwing an error when the limit is exceeded, allowing you to catch and handle such cases.
For similar reasons, by default qs will only parse up to 1000 parameters. This can be overridden by passing a parameterLimit option:
var limited = qs.parse('a=b&c=d', { parameterLimit: 1 });
assert.deepEqual(limited, { a: 'b' });
If you want an error to be thrown whenever the a limit is exceeded (eg, parameterLimit, arrayLimit), set the throwOnLimitExceeded option to true. This option will generate a descriptive error if the query string exceeds a configured limit.
try {
qs.parse('a=1&b=2&c=3&d=4', { parameterLimit: 3, throwOnLimitExceeded: true });
} catch (err) {
assert(err instanceof Error);
assert.strictEqual(err.message, 'Parameter limit exceeded. Only 3 parameters allowed.');
}
When throwOnLimitExceeded is set to false (default), qs will parse up to the specified parameterLimit and ignore the rest without throwing an error.
To bypass the leading question mark, use ignoreQueryPrefix:
var prefixed = qs.parse('?a=b&c=d', { ignoreQueryPrefix: true });
assert.deepEqual(prefixed, { a: 'b', c: 'd' });
An optional delimiter can also be passed:
var delimited = qs.parse('a=b;c=d', { delimiter: ';' });
assert.deepEqual(delimited, { a: 'b', c: 'd' });
Delimiters can be a regular expression too:
var regexed = qs.parse('a=b;c=d,e=f', { delimiter: /[;,]/ });
assert.deepEqual(regexed, { a: 'b', c: 'd', e: 'f' });
Option allowDots can be used to enable dot notation:
var withDots = qs.parse('a.b=c', { allowDots: true });
assert.deepEqual(withDots, { a: { b: 'c' } });
Option decodeDotInKeys can be used to decode dots in keys
Note: it implies allowDots, so parse will error if you set decodeDotInKeys to true, and allowDots to false.
var withDots = qs.parse('name%252Eobj.first=John&name%252Eobj.last=Doe', { decodeDotInKeys: true });
assert.deepEqual(withDots, { 'name.obj': { first: 'John', last: 'Doe' }});
Option allowEmptyArrays can be used to allow empty array values in an object
var withEmptyArrays = qs.parse('foo[]&bar=baz', { allowEmptyArrays: true });
assert.deepEqual(withEmptyArrays, { foo: [], bar: 'baz' });
Option duplicates can be used to change the behavior when duplicate keys are encountered
assert.deepEqual(qs.parse('foo=bar&foo=baz'), { foo: ['bar', 'baz'] });
assert.deepEqual(qs.parse('foo=bar&foo=baz', { duplicates: 'combine' }), { foo: ['bar', 'baz'] });
assert.deepEqual(qs.parse('foo=bar&foo=baz', { duplicates: 'first' }), { foo: 'bar' });
assert.deepEqual(qs.parse('foo=bar&foo=baz', { duplicates: 'last' }), { foo: 'baz' });
Note that keys with bracket notation ([]) always combine into arrays, regardless of the duplicates setting:
assert.deepEqual(qs.parse('a=1&a=2&b[]=1&b[]=2', { duplicates: 'last' }), { 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 iso-8859-1:
var oldCharset = qs.parse('a=%A7', { charset: 'iso-8859-1' });
assert.deepEqual(oldCharset, { 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 charsetSentinel option.
If specified, the utf8 parameter will be omitted from the returned object.
It will be used to switch to iso-8859-1/utf-8 mode depending on how the checkmark is encoded.
Important: When you specify both the charset option and the charsetSentinel option, the charset will be overridden when the request contains a utf8 parameter from which the actual charset can be deduced.
In that sense the charset will behave as the default charset rather than the authoritative charset.
var detectedAsUtf8 = qs.parse('utf8=%E2%9C%93&a=%C3%B8', {
charset: 'iso-8859-1',
charsetSentinel: true
});
assert.deepEqual(detectedAsUtf8, { a: 'ø' });
// Browsers encode the checkmark as ✓ when submitting as iso-8859-1:
var detectedAsIso8859_1 = qs.parse('utf8=%26%2310003%3B&a=%F8', {
charset: 'utf-8',
charsetSentinel: true
});
assert.deepEqual(detectedAsIso8859_1, { a: 'ø' });
If you want to decode the &#...; syntax to the actual character, you can specify the interpretNumericEntities option as well:
var detectedAsIso8859_1 = qs.parse('a=%26%239786%3B', {
charset: 'iso-8859-1',
interpretNumericEntities: true
});
assert.deepEqual(detectedAsIso8859_1, { a: '☺' });
It also works when the charset has been detected in charsetSentinel mode.
qs can also parse arrays using a similar [] notation:
var withArray = qs.parse('a[]=b&a[]=c');
assert.deepEqual(withArray, { a: ['b', 'c'] });
You may specify an index as well:
var withIndexes = qs.parse('a[1]=c&a[0]=b');
assert.deepEqual(withIndexes, { a: ['b', 'c'] });
Note that the only difference between an index in an array and a key in an object is that the value between the brackets must be a number to create an array. When creating arrays with specific indices, qs will compact a sparse array to only the existing values preserving their order:
var noSparse = qs.parse('a[1]=b&a[15]=c');
assert.deepEqual(noSparse, { a: ['b', 'c'] });
You may also use allowSparse option to parse sparse arrays:
var sparseArray = qs.parse('a[1]=2&a[3]=5', { allowSparse: true });
assert.deepEqual(sparseArray, { a: [, '2', , '5'] });
Note that an empty string is also a value, and will be preserved:
var withEmptyString = qs.parse('a[]=&a[]=b');
assert.deepEqual(withEmptyString, { a: ['', 'b'] });
var withIndexedEmptyString = qs.parse('a[0]=b&a[1]=&a[2]=c');
assert.deepEqual(withIndexedEmptyString, { a: ['b', '', 'c'] });
qs will also limit arrays to a maximum of 20 elements.
Any array members with an index of 20 or greater will instead be converted to an object 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 array.
var withMaxIndex = qs.parse('a[100]=b');
assert.deepEqual(withMaxIndex, { a: { '100': 'b' } });
This limit can be overridden by passing an arrayLimit option:
var withArrayLimit = qs.parse('a[1]=b', { arrayLimit: 0 });
assert.deepEqual(withArrayLimit, { a: { '1': 'b' } });
If you want to throw an error whenever the array limit is exceeded, set the throwOnLimitExceeded option to true. This option will generate a descriptive error if the query string exceeds a configured limit.
try {
qs.parse('a[1]=b', { arrayLimit: 0, throwOnLimitExceeded: true });
} catch (err) {
assert(err instanceof Error);
assert.strictEqual(err.message, 'Array limit exceeded. Only 0 elements allowed in an array.');
}
When throwOnLimitExceeded is set to false (default), qs will parse up to the specified arrayLimit and if the limit is exceeded, the array will instead be converted to an object with the index as the key
To prevent array syntax (a[], a[0]) from being parsed as arrays, set parseArrays to false.
Note that duplicate keys (e.g. a=b&a=c) may still produce arrays when duplicates is 'combine' (the default).
var noParsingArrays = qs.parse('a[]=b', { parseArrays: false });
assert.deepEqual(noParsingArrays, { a: { '0': 'b' } });
If you mix notations, qs will merge the two items into an object:
var mixedNotation = qs.parse('a[0]=b&a[b]=c');
assert.deepEqual(mixedNotation, { a: { '0': 'b', b: 'c' } });
When a key appears as both a plain value and an object, qs will by default wrap the conflicting values in an array (strictMerge defaults to true):
assert.deepEqual(qs.parse('a[b]=c&a=d'), { a: [{ b: 'c' }, 'd'] });
assert.deepEqual(qs.parse('a=d&a[b]=c'), { a: ['d', { b: 'c' }] });
To restore the legacy behavior (where the primitive is used as a key with value true), set strictMerge to false:
assert.deepEqual(qs.parse('a[b]=c&a=d', { strictMerge: false }), { a: { b: 'c', d: true } });
You can also create arrays of objects:
var arraysOfObjects = qs.parse('a[][b]=c');
assert.deepEqual(arraysOfObjects, { a: [{ b: 'c' }] });
Some people use comma to join array, qs can parse it:
var arraysOfObjects = qs.parse('a=b,c', { comma: true })
assert.deepEqual(arraysOfObjects, { a: ['b', 'c'] })
(this cannot convert nested objects, such as a={b:1},{c:d})
By default, all values are parsed as strings. This behavior will not change and is explained in issue #91.
var primitiveValues = qs.parse('a=15&b=true&c=null');
assert.deepEqual(primitiveValues, { a: '15', b: 'true', c: 'null' });
If you wish to auto-convert values which look like numbers, booleans, and other values into their primitive counterparts, you can use the query-types Express JS middleware which will auto-convert all request query parameters.
qs.stringify(object, [options]);
When stringifying, qs by default URI encodes output. Objects are stringified as you would expect:
assert.equal(qs.stringify({ a: 'b' }), 'a=b');
assert.equal(qs.stringify({ a: { b: 'c' } }), 'a%5Bb%5D=c');
This encoding can be disabled by setting the encode option to false:
var unencoded = qs.stringify({ a: { b: 'c' } }, { encode: false });
assert.equal(unencoded, 'a[b]=c');
Encoding can be disabled for keys by setting the encodeValuesOnly option to true:
var encodedValues = qs.stringify(
{ a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] },
{ encodeValuesOnly: true }
);
assert.equal(encodedValues,'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 encoding method set as encoder option:
var encoded = qs.stringify({ a: { b: 'c' } }, { encoder: function (str) {
// Passed in values `a`, `b`, `c`
return // Return encoded string
}})
(Note: the encoder option does not apply if encode is false)
Analogue to the encoder there is a decoder option for parse to override decoding of properties and values:
var decoded = qs.parse('x=z', { decoder: function (str) {
// Passed in values `x`, `z`
return // Return decoded string
}})
You can encode keys and values using different logic by using the type argument provided to the encoder:
var encoded = qs.stringify({ a: { b: 'c' } }, { encoder: function (str, defaultEncoder, charset, type) {
if (type === 'key') {
return // Encoded key
} else if (type === 'value') {
return // Encoded value
}
}})
The type argument is also provided to the decoder:
var decoded = qs.parse('x=z', { decoder: function (str, defaultDecoder, charset, type) {
if (type === 'key') {
return // Decoded key
} else if (type === 'value') {
return // Decoded value
}
}})
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 arrays are stringified, they follow the arrayFormat option, which defaults to indices:
qs.stringify({ a: ['b', 'c', 'd'] });
// 'a[0]=b&a[1]=c&a[2]=d'
You may override this by setting the indices option to false, or to be more explicit, the arrayFormat option to repeat:
qs.stringify({ a: ['b', 'c', 'd'] }, { indices: false });
// 'a=b&a=c&a=d'
You may use the arrayFormat option to specify the format of the output array:
qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'indices' })
// 'a[0]=b&a[1]=c'
qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'brackets' })
// 'a[]=b&a[]=c'
qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'repeat' })
// 'a=b&a=c'
qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'comma' })
// 'a=b,c'
Note: when using arrayFormat set to 'comma', you can also pass the commaRoundTrip option set to true or false, to append [] on single-item arrays, so that they can round trip through a parse.
When objects are stringified, by default they use bracket notation:
qs.stringify({ a: { b: { c: 'd', e: 'f' } } });
// 'a[b][c]=d&a[b][e]=f'
You may override this to use dot notation by setting the allowDots option to true:
qs.stringify({ a: { b: { c: 'd', e: 'f' } } }, { allowDots: true });
// 'a.b.c=d&a.b.e=f'
You may encode the dot notation in the keys of object with option encodeDotInKeys by setting it to true:
Note: it implies allowDots, so stringify will error if you set decodeDotInKeys to true, and allowDots to false.
Caveat: when encodeValuesOnly is true as well as encodeDotInKeys, only dots in keys and nothing else will be encoded.
qs.stringify({ "name.obj": { "first": "John", "last": "Doe" } }, { allowDots: true, encodeDotInKeys: true })
// 'name%252Eobj.first=John&name%252Eobj.last=Doe'
You may allow empty array values by setting the allowEmptyArrays option to true:
qs.stringify({ foo: [], bar: 'baz' }, { allowEmptyArrays: true });
// 'foo[]&bar=baz'
Empty strings and null values will omit the value, but the equals sign (=) remains in place:
assert.equal(qs.stringify({ a: '' }), 'a=');
Key with no values (such as an empty object or array) will return nothing:
assert.equal(qs.stringify({ a: [] }), '');
assert.equal(qs.stringify({ a: {} }), '');
assert.equal(qs.stringify({ a: [{}] }), '');
assert.equal(qs.stringify({ a: { b: []} }), '');
assert.equal(qs.stringify({ a: { b: {}} }), '');
Properties that are set to undefined will be omitted entirely:
assert.equal(qs.stringify({ a: null, b: undefined }), 'a=');
The query string may optionally be prepended with a question mark:
assert.equal(qs.stringify({ a: 'b', c: 'd' }, { addQueryPrefix: true }), '?a=b&c=d');
Note that when the output is an empty string, the prefix will not be added:
assert.equal(qs.stringify({}, { addQueryPrefix: true }), '');
The delimiter may be overridden with stringify as well:
assert.equal(qs.stringify({ a: 'b', c: 'd' }, { delimiter: ';' }), 'a=b;c=d');
If you only want to override the serialization of Date objects, you can provide a serializeDate option:
var date = new Date(7);
assert.equal(qs.stringify({ a: date }), 'a=1970-01-01T00:00:00.007Z'.replace(/:/g, '%3A'));
assert.equal(
qs.stringify({ a: date }, { serializeDate: function (d) { return d.getTime(); } }),
'a=7'
);
You may use the sort option to affect the order of parameter keys:
function alphabeticalSort(a, b) {
return a.localeCompare(b);
}
assert.equal(qs.stringify({ a: 'c', z: 'y', b : 'f' }, { sort: alphabeticalSort }), 'a=c&b=f&z=y');
Finally, you can use the filter option to restrict which keys will be included in the stringified output.
If you pass a function, it will be called for each key to obtain the replacement value.
Otherwise, if you pass an array, it will be used to select properties and array indices for stringification:
function filterFunc(prefix, value) {
if (prefix == 'b') {
// Return an `undefined` value to omit a property.
return;
}
if (prefix == 'e[f]') {
return value.getTime();
}
if (prefix == 'e[g][0]') {
return value * 2;
}
return value;
}
qs.stringify({ a: 'b', c: 'd', e: { f: new Date(123), g: [2] } }, { filter: filterFunc });
// 'a=b&c=d&e[f]=123&e[g][0]=4'
qs.stringify({ a: 'b', c: 'd', e: 'f' }, { filter: ['a', 'e'] });
// 'a=b&e=f'
qs.stringify({ a: ['b', 'c', 'd'], e: 'f' }, { filter: ['a', 0, 2] });
// 'a[0]=b&a[2]=d'
You could also use filter to inject custom serialization for user defined types.
Consider you're working with some api that expects query strings of the format for ranges:
https://domain.com/endpoint?range=30...70
For which you model as:
class Range {
constructor(from, to) {
this.from = from;
this.to = to;
}
}
You could inject a custom serializer to handle values of this type:
qs.stringify(
{
range: new Range(30, 70),
},
{
filter: (prefix, value) => {
if (value instanceof Range) {
return `${value.from}...${value.to}`;
}
// serialize the usual way
return value;
},
}
);
// range=30...70
null valuesBy default, null values are treated like empty strings:
var withNull = qs.stringify({ a: null, b: '' });
assert.equal(withNull, 'a=&b=');
Parsing does not distinguish between parameters with and without equal signs. Both are converted to empty strings.
var equalsInsensitive = qs.parse('a&b=');
assert.deepEqual(equalsInsensitive, { a: '', b: '' });
To distinguish between null values and empty strings use the strictNullHandling flag. In the result string the null
values have no = sign:
var strictNull = qs.stringify({ a: null, b: '' }, { strictNullHandling: true });
assert.equal(strictNull, 'a&b=');
To parse values without = back to null use the strictNullHandling flag:
var parsedStrictNull = qs.parse('a&b=', { strictNullHandling: true });
assert.deepEqual(parsedStrictNull, { a: null, b: '' });
To completely skip rendering keys with null values, use the skipNulls flag:
var nullsSkipped = qs.stringify({ a: 'b', c: null}, { skipNulls: true });
assert.equal(nullsSkipped, 'a=b');
If you're communicating with legacy systems, you can switch to iso-8859-1 using the charset option:
var iso = qs.stringify({ æ: 'æ' }, { charset: 'iso-8859-1' });
assert.equal(iso, '%E6=%E6');
Characters that don't exist in iso-8859-1 will be converted to numeric entities, similar to what browsers do:
var numeric = qs.stringify({ a: '☺' }, { charset: 'iso-8859-1' });
assert.equal(numeric, 'a=%26%239786%3B');
You can use the charsetSentinel option to announce the character by including an utf8=✓ parameter with the proper encoding if the checkmark, similar to what Ruby on Rails and others do when submitting forms.
var sentinel = qs.stringify({ a: '☺' }, { charsetSentinel: true });
assert.equal(sentinel, 'utf8=%E2%9C%93&a=%E2%98%BA');
var isoSentinel = qs.stringify({ a: 'æ' }, { charsetSentinel: true, charset: 'iso-8859-1' });
assert.equal(isoSentinel, 'utf8=%26%2310003%3B&a=%E6');
By default the encoding and decoding of characters is done in utf-8, and iso-8859-1 support is also built in via the charset parameter.
If you wish to encode querystrings to a different character set (i.e.
Shift JIS) you can use the
qs-iconv library:
var encoder = require('qs-iconv/encoder')('shift_jis');
var shiftJISEncoded = qs.stringify({ a: 'こんにちは!' }, { encoder: encoder });
assert.equal(shiftJISEncoded, 'a=%82%B1%82%F1%82%C9%82%BF%82%CD%81I');
This also works for decoding of query strings:
var decoder = require('qs-iconv/decoder')('shift_jis');
var obj = qs.parse('a=%82%B1%82%F1%82%C9%82%BF%82%CD%81I', { decoder: decoder });
assert.deepEqual(obj, { a: 'こんにちは!' });
RFC3986 used as default option and encodes ' ' to %20 which is backward compatible. In the same time, output can be stringified as per RFC1738 with ' ' equal to '+'.
assert.equal(qs.stringify({ a: 'b c' }), 'a=b%20c');
assert.equal(qs.stringify({ a: 'b c' }, { format : 'RFC3986' }), 'a=b%20c');
assert.equal(qs.stringify({ a: 'b c' }, { format : 'RFC1738' }), 'a=b+c');
Please email @ljharb or see https://tidelift.com/security if you have a potential security vulnerability to report.
Available as part of the Tidelift Subscription
The maintainers of qs and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. Learn more.
qs logo by NUMI: