Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: allow parsing of empty keys in objects #433

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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(obj, {});
```

It is possible to include empty keys by using `allowEmptyKeys` flag:

```javascript
var obj = qs.parse("=1&=2", { allowEmptyKeys: true });
assert.deepEqual(obj, { "": ["1","2"] });
```

### Stringifying

[](#preventEval)
Expand Down
9 changes: 7 additions & 2 deletions lib/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ var isArray = Array.isArray;

var defaults = {
allowDots: false,
allowEmptyKeys: false,
allowPrototypes: false,
allowSparse: false,
arrayLimit: 20,
Expand Down Expand Up @@ -84,6 +85,9 @@ var parseValues = function parseQueryStringValues(str, options) {
var key, val;
if (pos === -1) {
key = options.decoder(part, defaults.decoder, charset, 'key');
if (key === '') {
continue;
ljharb marked this conversation as resolved.
Show resolved Hide resolved
}
val = options.strictNullHandling ? null : '';
} else {
key = options.decoder(part.slice(0, pos), defaults.decoder, charset, 'key');
Expand Down Expand Up @@ -149,7 +153,7 @@ var parseObject = function (chain, val, options, valuesParsed) {
};

var parseKeys = function parseQueryStringKeys(givenKey, val, options, valuesParsed) {
if (!givenKey) {
if (!givenKey && (!options.allowEmptyKeys || givenKey !== '')) {
return;
}

Expand All @@ -169,7 +173,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) {
Expand Down Expand Up @@ -218,6 +222,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,
Expand Down
54 changes: 27 additions & 27 deletions test/empty-keys-cases.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 34 additions & 1 deletion test/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -889,10 +889,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' }
Comment on lines +916 to +926
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's discuss these TODOs. what are your thoughts?

Copy link
Contributor Author

@Coobaha Coobaha May 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we parse empty keys I assume output should be equal to key-full strings?

  • If we use a[b][0]=2&a[b][1]=3 the output object is { a: { b: [ '2', '3' ] } }
  • when we replace a and b with empty strings in this object: { '': { '': [ '2', '3' ] } }
    • so query string equivalent should be [][0]=2&[][1]=3 which is <EMPTY_KEY>[<EMPTY_KEY>][0]=2

Current output for [][0]=2&[][1]=3 without empty keys flag is { 0: '2', 1: '3' }

);

st.end();
});
});
1 change: 1 addition & 0 deletions test/stringify.js
Original file line number Diff line number Diff line change
Expand Up @@ -958,6 +958,7 @@ test('stringifies empty keys', function (t) {
emptyTestCases.forEach(function (testCase) {
t.test('stringifies an object with empty string key with ' + testCase.input, function (st) {
st.deepEqual(qs.stringify(testCase.withEmptyKeys, { encode: false }), testCase.stringifyOutput);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we also add a test case where the input is a stringified noEmptyKeys?

st.deepEqual(qs.stringify(testCase.noEmptyKeys, { encode: false }), testCase.stringifyOutputNoEmpty);

st.end();
});
Expand Down