qs icon indicating copy to clipboard operation
qs copied to clipboard

Mixed object types and JSON-API

Open alex94cp opened this issue 8 years ago • 7 comments

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.

alex94cp avatar Jul 24 '15 20:07 alex94cp

that is strange.. i'll check it out

nlf avatar Jul 30 '15 16:07 nlf

@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'] }?

ljharb avatar Jul 22 '16 08:07 ljharb

Yeah I'm experiencing this too and getting hashes instead of arrays. Same format of object -> array -> objects

jbhatab avatar Feb 02 '17 10:02 jbhatab

@jbhatab what would your expectation be in response to my question?

ljharb avatar Feb 02 '17 17:02 ljharb

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 avatar Mar 16 '24 19:03 wavedeck

@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?

ljharb avatar Mar 16 '24 20:03 ljharb

@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.

wavedeck avatar Mar 17 '24 00:03 wavedeck