diff --git a/README.md b/README.md index 01263804..60270dc4 100644 --- a/README.md +++ b/README.md @@ -299,6 +299,23 @@ 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](https://github.com/xpepermint/query-types) which will auto-convert all request query parameters. + +### Parsing empty keys + +By default, empty keys are omitted after parsing: + +```javascript +var obj = qs.parse("=1&=2"); +assert.deepEqual(withNull, {}); +``` + +It is possible to include empty keys by using `allowEmptyKeys` flag: + +```javascript +var obj = qs.parse("=1&=2", { allowEmptyKeys: true }); +assert.deepEqual(withNull, { "": ["1","2"] }); +``` + ### Stringifying [](#preventEval) diff --git a/lib/parse.js b/lib/parse.js index a4ac4fa0..2a033a55 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -7,6 +7,7 @@ var isArray = Array.isArray; var defaults = { allowDots: false, + allowEmptyKeys: false, allowPrototypes: false, allowSparse: false, arrayLimit: 20, @@ -83,6 +84,9 @@ var parseValues = function parseQueryStringValues(str, options) { var key, val; if (pos === -1) { key = options.decoder(part, defaults.decoder, charset, 'key'); + if (key === '') { + continue; + } val = options.strictNullHandling ? null : ''; } else { key = options.decoder(part.slice(0, pos), defaults.decoder, charset, 'key'); @@ -148,7 +152,7 @@ var parseObject = function (chain, val, options, valuesParsed) { }; var parseKeys = function parseQueryStringKeys(givenKey, val, options, valuesParsed) { - if (!givenKey) { + if (!givenKey && (!options.allowEmptyKeys || givenKey !== '')) { return; } @@ -168,7 +172,7 @@ var parseKeys = function parseQueryStringKeys(givenKey, val, options, valuesPars // Stash the parent if it exists var keys = []; - if (parent) { + if (parent || (options.allowEmptyKeys && parent === '')) { // If we aren't using plain objects, optionally prefix keys that would overwrite object prototype properties if (!options.plainObjects && has.call(Object.prototype, parent)) { if (!options.allowPrototypes) { @@ -217,6 +221,7 @@ var normalizeParseOptions = function normalizeParseOptions(opts) { return { allowDots: typeof opts.allowDots === 'undefined' ? defaults.allowDots : !!opts.allowDots, + allowEmptyKeys: typeof opts.allowEmptyKeys === 'undefined' ? defaults.allowEmptyKeys : !!opts.allowEmptyKeys, allowPrototypes: typeof opts.allowPrototypes === 'boolean' ? opts.allowPrototypes : defaults.allowPrototypes, allowSparse: typeof opts.allowSparse === 'boolean' ? opts.allowSparse : defaults.allowSparse, arrayLimit: typeof opts.arrayLimit === 'number' ? opts.arrayLimit : defaults.arrayLimit, diff --git a/test/parse.js b/test/parse.js index 11233ba1..0b21f423 100644 --- a/test/parse.js +++ b/test/parse.js @@ -847,10 +847,43 @@ test('parse()', function (t) { test('parses empty keys', function (t) { emptyTestCases.forEach(function (testCase) { + t.test('parses an object with empty string key with ' + testCase.input, function (st) { + st.deepEqual(qs.parse(testCase.input, { allowEmptyKeys: true }), testCase.withEmptyKeys); + st.deepEqual(qs.parse(testCase.stringifyOutput, { allowEmptyKeys: true }), testCase.withEmptyKeys); + + st.end(); + }); + t.test('skips empty string key with ' + testCase.input, function (st) { - st.deepEqual(qs.parse(testCase.input), testCase.noEmptyKeys); + st.deepEqual( + qs.parse(testCase.input), + testCase.noEmptyKeys + ); + + st.deepEqual( + qs.parse(testCase.input, { allowEmptyKeys: false }), + testCase.noEmptyKeys + ); st.end(); }); }); + + t.test('edge case with object/arrays', function (st) { + st.deepEqual( + qs.parse('[][0]=2&[][1]=3', { allowEmptyKeys: true }), + { '': { '': ['2', '3'] } }, + 'array/object conversion', + { skip: 'TODO: figure out what this should do' } + ); + + st.deepEqual( + qs.parse('[][0]=2&[][1]=3&[a]=2', { allowEmptyKeys: true }), + { '': { '': ['2', '3'], a: '2' } }, + 'array/object conversion', + { skip: 'TODO: figure out what this should do' } + ); + + st.end(); + }); });