Skip to content

Commit cb41a54

Browse files
committed
[New] parse: add strictMerge option to wrap object/primitive conflicts in an array
When `strictMerge` is `true` (the default), merging a primitive into an object wraps both in an array instead of using the primitive as a key with value `true`. Set `strictMerge` to `false` to restore legacy behavior. Fixes #425. Fixes #122.
1 parent 88e1563 commit cb41a54

5 files changed

Lines changed: 56 additions & 4 deletions

File tree

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,19 @@ var mixedNotation = qs.parse('a[0]=b&a[b]=c');
330330
assert.deepEqual(mixedNotation, { a: { '0': 'b', b: 'c' } });
331331
```
332332

333+
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`):
334+
335+
```javascript
336+
assert.deepEqual(qs.parse('a[b]=c&a=d'), { a: [{ b: 'c' }, 'd'] });
337+
assert.deepEqual(qs.parse('a=d&a[b]=c'), { a: ['d', { b: 'c' }] });
338+
```
339+
340+
To restore the legacy behavior (where the primitive is used as a key with value `true`), set `strictMerge` to `false`:
341+
342+
```javascript
343+
assert.deepEqual(qs.parse('a[b]=c&a=d', { strictMerge: false }), { a: { b: 'c', d: true } });
344+
```
345+
333346
You can also create arrays of objects:
334347

335348
```javascript

lib/parse.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ var defaults = {
2525
parseArrays: true,
2626
plainObjects: false,
2727
strictDepth: false,
28+
strictMerge: true,
2829
strictNullHandling: false,
2930
throwOnLimitExceeded: false
3031
};
@@ -339,6 +340,7 @@ var normalizeParseOptions = function normalizeParseOptions(opts) {
339340
parseArrays: opts.parseArrays !== false,
340341
plainObjects: typeof opts.plainObjects === 'boolean' ? opts.plainObjects : defaults.plainObjects,
341342
strictDepth: typeof opts.strictDepth === 'boolean' ? !!opts.strictDepth : defaults.strictDepth,
343+
strictMerge: typeof opts.strictMerge === 'boolean' ? !!opts.strictMerge : defaults.strictMerge,
342344
strictNullHandling: typeof opts.strictNullHandling === 'boolean' ? opts.strictNullHandling : defaults.strictNullHandling,
343345
throwOnLimitExceeded: typeof opts.throwOnLimitExceeded === 'boolean' ? opts.throwOnLimitExceeded : false
344346
};

lib/utils.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ var merge = function merge(target, source, options) {
8585
var newIndex = getMaxIndex(target) + 1;
8686
target[newIndex] = source;
8787
setMaxIndex(target, newIndex);
88+
} else if (options && options.strictMerge) {
89+
return [target, source];
8890
} else if (
8991
(options && (options.plainObjects || options.allowPrototypes))
9092
|| !has.call(Object.prototype, source)

test/parse.js

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -784,32 +784,60 @@ test('parse()', function (t) {
784784

785785
t.test('add keys to objects', function (st) {
786786
st.deepEqual(
787-
qs.parse('a[b]=c&a=d'),
787+
qs.parse('a[b]=c&a=d', { strictMerge: false }),
788788
{ a: { b: 'c', d: true } },
789789
'can add keys to objects'
790790
);
791791

792792
st.deepEqual(
793-
qs.parse('a[b]=c&a=toString'),
793+
qs.parse('a[b]=c&a=toString', { strictMerge: false }),
794794
{ a: { b: 'c' } },
795795
'can not overwrite prototype'
796796
);
797797

798798
st.deepEqual(
799-
qs.parse('a[b]=c&a=toString', { allowPrototypes: true }),
799+
qs.parse('a[b]=c&a=toString', { strictMerge: false, allowPrototypes: true }),
800800
{ a: { b: 'c', toString: true } },
801801
'can overwrite prototype with allowPrototypes true'
802802
);
803803

804804
st.deepEqual(
805-
qs.parse('a[b]=c&a=toString', { plainObjects: true }),
805+
qs.parse('a[b]=c&a=toString', { strictMerge: false, plainObjects: true }),
806806
{ __proto__: null, a: { __proto__: null, b: 'c', toString: true } },
807807
'can overwrite prototype with plainObjects true'
808808
);
809809

810810
st.end();
811811
});
812812

813+
t.test('strictMerge wraps object and primitive into an array', function (st) {
814+
st.deepEqual(
815+
qs.parse('a[b]=c&a=d'),
816+
{ a: [{ b: 'c' }, 'd'] },
817+
'object then primitive produces array'
818+
);
819+
820+
st.deepEqual(
821+
qs.parse('a=d&a[b]=c'),
822+
{ a: ['d', { b: 'c' }] },
823+
'primitive then object produces array'
824+
);
825+
826+
st.deepEqual(
827+
qs.parse('a[b]=c&a=toString'),
828+
{ a: [{ b: 'c' }, 'toString'] },
829+
'prototype-colliding value is preserved in array'
830+
);
831+
832+
st.deepEqual(
833+
qs.parse('a[b]=c&a=toString', { plainObjects: true }),
834+
{ __proto__: null, a: [{ __proto__: null, b: 'c' }, 'toString'] },
835+
'plainObjects preserved in array wrapping'
836+
);
837+
838+
st.end();
839+
});
840+
813841
t.test('dunder proto is ignored', function (st) {
814842
var payload = 'categories[__proto__]=login&categories[__proto__]&categories[length]=42';
815843
var result = qs.parse(payload, { allowPrototypes: true });

test/utils.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,13 @@ test('merge()', function (t) {
9595
s2t.end();
9696
});
9797

98+
st.test('with strictMerge, wraps object and primitive in array', function (s2t) {
99+
var obj = { foo: 'bar' };
100+
var merged = utils.merge(obj, 'baz', { strictMerge: true });
101+
s2t.deepEqual(merged, [{ foo: 'bar' }, 'baz'], 'wraps in array with strictMerge');
102+
s2t.end();
103+
});
104+
98105
st.test('merges overflow object into primitive', function (s2t) {
99106
// Create an overflow object via combine: 2 elements (indices 0-1) with limit 0
100107
var overflow = utils.combine(['a'], 'b', 0, false);

0 commit comments

Comments
 (0)