ljharb / qs

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

Option to preserve empty arrays and objects at "qs.stringify()" #362

Closed vdenisenko-waverley closed 9 months ago

vdenisenko-waverley commented 4 years ago

Documentation saying:

Key with no values (such as an empty object or array) will return nothing

However there are cases when this behavior is not desirable. It brings inconsistencies between stringify and parse methods, meaning same object can't be stringified and then parsed. Example:

qs.stringify({ foo: [], bar: 1 })
// -> "bar=1"

qs.parse("bar=1")
// -> {bar: "1"}

From above example foo field is lost.

ljharb commented 4 years ago

This is how most backends interpret it - an empty array means there's nothing there, so there's nothing lost. Can you elaborate on the use cases?

vdenisenko-waverley commented 4 years ago

I'm trying to persist query string which contains complex JSON structure on page refresh. This is why inconsistency matters for my case.

Currently I have to iterate through all JSON fields recursively and append missing fields, to properly initialize JSON on page refresh - which looks not clean and brings unnecessary complication to process.

ljharb commented 4 years ago

Is there a reason why the code interacting with your complex structure can't create the structure when needed, rather than requiring it be initialized already?

vdenisenko-waverley commented 4 years ago

This is exactly what I have to do, just noticed it is strange that I can't get back same object which was stringified.

ljharb commented 4 years ago

JSON itself doesn't provide that guarantee; generally stringify(parse(stringify(x)) === stringify(x), but not necessarily parse(stringify(parse(x))) giving you the same object as parse(x).

shashi8shekhar commented 4 years ago

Did Anybody find workaround for this issue. my use case is, i am trying to render client using parsed data stored in url, i need to preserve all the state during qs.stringify().

nikkwong commented 4 years ago

This is necessary for cases like the mongo{ query: {$eq: []}}

damonblack commented 4 years ago

Seems like sending a query argument that might be empty (like an array of filter fields) is a pretty common use case. I'm fighting with this now.

ljharb commented 4 years ago

I could see a case where foo: [] serializes to foo[] - no equals sign - but i can't see how it would make sense in anything but the "brackets" and "comma" arrayFormat cases, since there's no way to represent an empty array otherwise.

lvdang commented 4 years ago

I am having the same issue. Was there a fix to this?

lvdang commented 4 years ago

@damonblack @nikkwong @vdenisenko-waverley - if you pass in JSON.stringify([]) this worked for me.

const query = { showItems: JSON.stringify([])}

jmbtutor commented 3 years ago

I'm working on a project where the query parameters are used to specify overrides, falling back to defaults for missing parameters. In this scenario, foo=[] (where [] represents an empty array) tells the backend to use an empty array for foo; this is distinct from omitting foo, which tells the backend to use defaults (and this default is not empty). We've worked around this for now by doing our own processing on a string value (joining/splitting on a delimiter with the empty string representing an empty array), but it would be nice to have this handled by the library.

ljharb commented 3 years ago

@jmbtutor would serializing to just foo[], without an equals sign, be sufficient to convey that semantic?

If so, I'd be willing to accept a PR for an option to both parse and stringify that explicitly preserved empty arrays and objects in this manner.

Notmeomg commented 3 years ago

I ran into a case where I'm using Joi array conditionals to control the attributes returned by the model. When there's no attributes in the req we use defaults, when the attributes array is empty we don't return any attributes, only associations. Was surprised to find out we couldn't pass empty arrays with qs. Is there a better solution to passing empty arrays in the query?

const query = { where: { id: 101 }, attributes: [], include: ['addresses'], }

wisetwo commented 3 years ago

This is how most backends interpret it - an empty array means there's nothing there, so there's nothing lost. Can you elaborate on the use cases?

Sometimes the backend parse the query string to know what was requested, for example, an update request needs to know which field needs updating; If a field of array needs to be cleared, the backend might be expecting list: [], but the result might not be achieved if the library just drop this field.

ljharb commented 3 years ago

@wisetwo in which of these scenarios can the backend not be changed to adapt to the frontend (which it should always do, since the frontend should never have to change for the sake of the backend)?

jmbtutor commented 3 years ago

would serializing to just foo[], without an equals sign, be sufficient to convey that semantic?

@ljharb Sorry, I just noticed this ping. As long as that deserializes to an empty array, I think that should be fine. It communicates that the option is specified and that the value is empty.

webdev-lee commented 3 years ago

I've just spent a few hours debugging this exact issue, I send an object containing various properties to my back end, some of which are arrays, in the case that these are empty they are essentially being ignored. I do some processing on the object and then return the modified object back to my front end. With the empty arrays now missing my reactive Vue JS templates on the front end were erroring as I was checking the length of the arrays, which in some instances no longer exist.

I think there are some pretty solid use cases for not ignoring empty arrays. The workaround is having to check if these properties exist and then append empty arrays, ideally I would just be returning all the original object properties.

iMoses-Apiiro commented 3 years ago

Turns out that in ASP.net if you define a query parameter from type dictionary and don't pass a value it will automatically be populated by the all query string options. This is a horrible behaviour which they claim is a bug and not a feature :| https://stackoverflow.com/questions/45997570/asp-net-core-fromquery-getting-invalid-parameters-with-dot-sign-inside/46003081#46003081

Nonetheless, it'd be very helpful if qs define an option to treat empty values (objects, array) the same as null and output an empty reference. Very very helpful...

zcrkey commented 2 years ago

I am having the same issue. Was there a fix to this?

ljharb commented 2 years ago

@zcrkey nope, if there was a fix the issue would be closed, or linked to an open PR.

jose-codaio commented 2 years ago

At the very least, how about parametrizing the behavior to use an empty string '' instead of getting rid of the argument altogether? For my use case, I can work with a 0-value like '' but not with an undefined value.

This is still not ideal. The ideal would be that encoding and decoding could happen back and forth without surprises.

ljharb commented 2 years ago

@jose-codaio https://github.com/ljharb/qs/issues/362#issuecomment-750424101 already says i'd accept a PR that added an option.

Surprises are always inevitable, because everybody's expectations are different.

jose-codaio commented 2 years ago

@jose-codaio #362 (comment) already says i'd accept a PR that added an option.

Sorry, missed that.

Surprises are always inevitable, because everybody's expectations are different.

By "surprises", I meant anything preventing the encoding process from being reversible. If an encoding process isn't reversible, then it just adds burden onto the requester. It'd be awesome if I could write the following with no problem.

stringify(parse(stringify(parse(...))))

I understand if we can't always live up to that, but that is the ideal. Otherwise, I'll need custom logic to handle these "surprises".

JSON itself doesn't provide that guarantee; generally stringify(parse(stringify(x)) === stringify(x), but not necessarily parse(stringify(parse(x))) giving you the same object as parse(x).

Out of curiosity: unless x includes non-JSON aspects (eg functions, non-enumerable properties), why would that not work?

ljharb commented 2 years ago

What is a "non-JSON aspect"? Try round-tripping a Date object, or a regex, or anything with a .toJSON method.

It's just not a reasonable expectation that a serialize/deserialize function is reversible for all input - only for a subset of input. It's a great goal, but it's not a reasonable expectation.

jose-codaio commented 2 years ago

What is a "non-JSON aspect"? Try round-tripping a Date object, or a regex, or anything with a .toJSON method.

Dates and regex's are not JSON. JSON is pretty much just:

(Wikipedia, ECMA publication)

That method is misleading, but it's probably just to easily dump an object into a JSON-compatible format.

It's just not a reasonable expectation that a serialize/deserialize function is reversible for all input - only for a subset of input. It's a great goal, but it's not a reasonable expectation.

Yeah, that's fair. 😔

zizhuxuaner commented 2 years ago

{ a: [], b:{ c: [], }, e: '111' } =====> a=&b[c]=&e=111

How to implement such parsing

ljharb commented 2 years ago

@zizhuxuaner since you filed #445, i'll hide this comment as off-topic.

HyopeR commented 2 years ago

I'm having the same problem. I solve my problem by doing a little trick on the frontend. But a parameter would be nice to have empty arrays behave differently.

ljharb commented 2 years ago

@HyopeR https://github.com/ljharb/qs/issues/362#issuecomment-750424101 is still waiting for a PR.

honia19 commented 11 months ago

Please make a PR, it is very strange functionality

ljharb commented 9 months ago

Fixed in #487.