ljharb / qs

A querystring parser with nesting support
BSD 3-Clause "New" or "Revised" License
8.5k stars 729 forks source link

Array of Objects should always have indices #422

Open jerrens opened 2 years ago

jerrens commented 2 years ago

In the scenario of stringify'ing the following object:

{
    string: [ 'A', 'B', 'C'],
    obj: [
        { field1: 'A.1', field2: 'A.2' },
        { field1: 'B.1', field2: 'B.2' }
   ]
}

I would like to encode the query string as: string=A&string=B&string=C&obj[0].field1=A.1&obj[0].field2=A.2&obj[1].field1=B.1&obj[1].field2=B.2

I've attempted to use the stringify options: { encodeValuesOnly: true, allowDots: true } along with various forms of the indices choices, but I can't seem to get the right combination.

Would it make sense if the stringify options of indices: false and/or arrayFormat: 'repeat' logic were to be updated to not include indices UNLESS the element type is an object - when it is an object, indices would need to be included because otherwise there is no way to know which fields the individual params should be reassembled into.

Current Attempts:

qs.stringify(testInput, { encodeValuesOnly: true, allowDots: true });
// Output: 'string[0]=A&string[1]=B&string[2]=C&obj[0].field1=A.1&obj[0].field2=A.2&obj[1].field1=B.1&obj[1].field2=B.2'

qs.stringify(testInput, { encodeValuesOnly: true, allowDots: true, indices: false });
// Output: 'string=A&string=B&string=C&obj.field1=A.1&obj.field2=A.2&obj.field1=B.1&obj.field2=B.2'

qs.stringify(testInput, { encodeValuesOnly: true, allowDots: true, arrayFormat: 'repeat' });
// Output: 'string=A&string=B&string=C&obj.field1=A.1&obj.field2=A.2&obj.field1=B.1&obj.field2=B.2'

In the second and third examples, are qs.parse'es back into:

{
    "string": [
        "A",
        "B",
        "C"
    ],
    "obj": {
        "field1": [
            "A.1",
            "B.1"
        ],
        "field2": [
            "A.2",
            "B.2"
        ]
    }
}

which make sense.

I have also confirmed that my desired format: string=A&string=B&string=C&obj[0].field1=A.1&obj[0].field2=A.2&obj[1].field1=B.1&obj[1].field2=B.2 does correctly parse into the original object, so no code change would be required on the parsing side.

ljharb commented 2 years ago

So, to clarify - you want to stringify into a mix of indices and also repeat?

What's your serverside where that's idiomatic?

ljharb commented 2 years ago

Note that doing something implicit here isn't an option, because [1, 'a', { b: 'c' }, { d: 'e' }] needs to be deterministically stringified in a way that matches the containing querystring.

jerrens commented 2 years ago

Hi @ljharb - thank you looking into this. I can see the potential problem with the array of mixed data types

To answer your first question, we are making a tool that supports permalinks for the URI. One of the query parameter fields is an array of string values and a second one needs to be an array of objects because it needs both a database name and an ID for each element in the array. We also have more fields to add in the future to the tool that would likely need multiple fields grouped together. The schema for the object we want to reconstruct is the same as the one provided in the example of the OP, just the names changed to be generic values.

The permalinks work as needed when the string array in stringified to string[n]=..., but when the square brackets is encoded, it is difficult for most people to read the %5B0%5D and visualize what it is. The option to only encode the value is very nice since that lets us build a more human-readable query string for the browser, but most browsers when loading the page will encode those square brackets, making the link harder to read again. For this reason, it would be nice to be able to not have indices when not absolutely necessary on array elements that are basic data types.

I attempted to use the filter option to create my own callback to handle the encoding of the simple array, but couldn't quite figure out the proper way to return the value. Maybe this is a path to investigate more.

As for the example array of mixed datatypes you mentioned as being problematic, I image the following string would be supported by the current parser to rebuild that array properly (I'm not on my development computer at the moment to try for myself).

Pseudo code to stringify: If the value is an array, Loop through each element of the array If the data type of the element is a number, string, etc, and indices = false or arrayFormat='repeat', encode as you do now (with no indices) If the data type of the element is an object, then encode it the way you do when indices = true, using the current index of the element in the array as the reference.

For example { arr: [1, 'a', { b: 'c' }, { d: 'e' }]} Would be stringified to: arr=1&arr=a&arr[2].b=c&arr[3].d=e

jerrens commented 2 years ago

It looks like that already works as hoped:

const qsObj = qs.parse('arr=1&arr=a&arr[2].b=c&arr[3].d=e', { allowDots: true });
JSON.stringify(qsObj, null, 4)

/*
{
    "arr": [
        "1",
        "a",
        {
            "b": "c"
        },
        {
            "d": "e"
        }
    ]
}
*/

The one exception is that the first element is a string instead of a number, but I think that will always be the case

ljharb commented 2 years ago

@jerrens yes, it will; all query params are always containers or strings unless you decode them differently.