qs
qs copied to clipboard
Mixed object types and JSON-API
If I understand correctly, qs will parse a query string like "foo=123" as "{ foo: 123 }", and "foo[first]=123&foo[second]=456" as "{ foo: { first: 123, second: 456 }}". But, what happens with mixed types seems a little weird:
qs.parse('foo=bar&foo[first]=1234')
{ foo: [ 'bar', { first: '1234' } ] }
qs.parse('foo=bar&foo[first]=1234&foo[second]=5678')
{ foo: { '0': 'bar', '1': { first: '1234' }, second: '5678' } }
The first example seems the most correct to me. This may seem like a rare use case, but the JSON-API standard makes heavy use of query strings like that.
that is strange.. i'll check it out
@alex94puchades does this mean that you would expect integer (or omitted) indexes to be in an array, and everything else to be in an object inside the array, and then collapsing consecutive objects? ie, foo[]=bar&foo[]=baz&foo[a]=1&foo[b]=1&foo[]=quux
would become { foo: ['bar', 'baz', { a: 1, b: 1 }, 'quux'] }
?
Yeah I'm experiencing this too and getting hashes instead of arrays. Same format of object -> array -> objects
@jbhatab what would your expectation be in response to my question?
I'd like to give an update on mixing object types with the JSON:API specification.
As of specification version 1.1, such query would be achieved using query parameter families in which the specification explicitly states:
The two parameters are named page[offset] and page[limit]; there is no single page parameter.
using parameter families, the correct querystring would be foo[]=bar&foo[first]=1234&foo[]=anotherExample
which this library handles as: { foo: { '0': 'bar', '1': 'anotherExample' first: '1234' } }
so when complying to v1.1 of the spec, you can easily get all non-keyed values without any weird behaviour like this:
const queryObject = qs.parse("foo[]=bar&foo[first]=1234&foo[]=anotherExample");
const fooValues = [];
for (const key in queryObject.foo) {
if (!isNaN(parseInt(key))) {
fooValues.push(obj.foo[key]);
}
}
console.log(fooValues) // => ['bar', 'anotherExample']
IMHO going forward: qs
should not switch between arrays and objects for the output of qs.parse
as this might lead to even more issues when trying to access a property of an object on the array.
@wavedeck so, to paraphrase, earlier versions of the JSON API spec would have required some behavior in qs to handle this use case, but the latest version of the spec no longer does, and this can be closed?
@wavedeck so, to paraphrase, earlier versions of the JSON API spec would have required some behavior in qs to handle this use case, but the latest version of the spec no longer does, and this can be closed?
The latest version encourages the use of query parameter families, where qs
behaves correctly so I'd say this issue can be closed when the goal is to only support JSON:API v1.1 and up.
Unrelated to JSON:API however is that qs
behaves differently between the two examples given by the author, making foo an array in the first and an object in the second example.
I've tested and confirmed the original issue with a possible scenario that might appear in the real world:
query | result |
---|---|
shipping=true &shipping[country]=DE | { shipping: [ 'true', { country: 'DE' } ] } |
shipping=true &shipping[taxRate]=19 &shipping[country]=DE &shipping[expedited]=true' | { shipping: { '0': 'true', '1': { taxRate: '19' }, country: 'DE', expedited: 'true' } } |
shipping=true &shipping[country]=DE &shipping[taxRate]=19 | { shipping: { '0': 'true', '1': { taxRate: '19' }, country: 'DE' } } |
notice how:
-
qs
converts shipping to an array when using less than 3 parameters. -
qs
converts shipping to an object and treats the first sub-key it encounters as a value -
qs
treats all other sub-keys as actual key-value pairs
I believe that the result should always be an object when it has sub-keys and creates an index key for all parent values regardless where the parent key appears in the query string.
query | result |
---|---|
shipping=true &shipping[taxRate]=19 &shipping[country]=DE &shipping[expedited]=true' | { shipping: { '0': 'true', taxRate: '19', country: 'DE', expedited: 'true' } |
shipping=true &shipping[taxRate]=19 &shipping[country]=DE | { shipping: { '0': 'true', taxRate: '19', country: 'DE' } |
shipping=true &shipping[country]=DE | { shipping: { '0': 'true', country: 'DE' } |
shipping=true &shipping[country]=DE & shipping=anotherTest | { shipping: { '0': 'true', '1': 'anotherTest', country: 'DE' } |
notice how:
- shipping is always an object simply because it has sub-keys like shipping[country]
- the shipping object emulates an array by using index keys like '0', '1'....
- parent values get an index key regardless of position (anotherTest has index 1 although its the last item)
this way, you can use either shipping[0]
or shipping["0"]
for parent values and either shipping['country']
or shipping.country
for sub-keys, leaving the most flexibility and a clear rule for developers to follow: bracket notation for parent values and bracket / dot notation for sub-keys
TLDR; While JSON:API v1.1 fixes the issue using query parameter families, another (unrelated) implementations that don't follow the JSON:API spec might also encounter this issue.