It's often helpful to keep around a collection of utility functions. I have one, pathEntries, that creates something like what Object.entries does, but instead of a string key, it has an array of string/integer keys giving the whole path in a nested object. The version used here includes only paths to root nodes. It would convert your input into this format:
[
[["season"], "winter"],
[["data", "month", 0], "December"],
[["data", "month", 1], "January"],
[["data", "month", 2], "February"]
]
Using that, it's quite simple:
const pathEntries = (obj) => Object (obj) === obj
? Object .entries (obj) .flatMap (([k, x]) => pathEntries (x) .map (
([p, v]) => [[Array .isArray (obj) ? Number (k) : k, ... p], v]
))
: [[[], obj]]
const toQuery = (obj) => pathEntries (obj) .map (
([[p, ...ps], v]) => `${p}${ ps .map (n => `[${ n }]`) .join ('') }=${ v }`
) .join ('&')
const obj = {season: 'winter', data: {month: ['December', 'January', 'February']}}
console .log (toQuery (obj))
Because we know that the paths cannot be empty, we can comfortably extract the first element from them and treat it differently. (It's not wrapped in [ - ]), and we can just build one part of our response string directly using the first node in the path, p, the remaining nodes, ps, and the value, v. Then we join these parts together with &, and we're done.
pathEntries is more interesting. If we're not working on an object, we simply return an array containing a single pair: an empty array for the path, and our input value. If it is an Object, then we use Object .entries to break it down into key-value pairs, recur on the values, and prepend the key to the path of each result.
For this problem we could simplify by replacing this line:
([p, v]) => [[Array .isArray (obj) ? Number (k) : k, ... p], v]
with
([p, v]) => [[k, ... p], v]
The original version yields enough information to reconstitute objects including arrays nested at any depth. With this change, we would turn them into plain objects with keys like "0", "1", etc. That's fine for this problem. But I usually choose the more powerful version, because I might want to reuse it in multiple places in an application.
Notes
User Mulan suggested that continually checking to see whether our key is numeric while we recur is sub-optimal. There are many ways to fix that, including storing it in an IIFE, using a call/bind helper, or using (abusing?) the fact that Object .entries returns a collection of pairs, two-element arrays, to default a third parameter, as demonstrated here:
const pathEntries = (obj) => Object (obj) === obj
? Object .entries (obj) .flatMap (
([k, x, k1 = Array .isArray (obj) ? Number (k) : k]) =>
pathEntries (x) .map (([p, v]) => [[k1, ... p], v])
)
: [[[], obj]]
Mulan also mentioned that the URLSearchParams API is a much better way to build robust URLs. This is entirely correct. We can do so with something like:
const pathEntries = (obj) => Object (obj) === obj
? Object .entries (obj) .flatMap (
([k, x, k1 = Array .isArray (obj) ? Number (k) : k]) =>
pathEntries (x) .map (([p, v]) => [[k1, ... p], v])
)
: [[[], obj]]
const toQuery = (obj) => pathEntries (obj) .map (
([[p, ...ps], v]) => [`${p}${ ps .map (n => `[${ n }]`) .join ('') }`, v]
) .reduce ((usp, [k, v]) => ((usp .append (k, v)), usp), new URLSearchParams (''))
const obj = {season: 'winter', data: {month: ['December', 'January', 'February']}}
console .log (toQuery (obj) .toString())
Note that the brackets are now correctly encoded. However this is not the exact output that was requested. You will have to decide if that's appropriate for your needs.