I have a simple object:
let obj = {
season: 'winter',
data: {
month: ['December', 'January', 'February']
}
};
I need to get a string like:
`season=winter&data[month][0]=December&data[month][1]=January&data[month][2]=February`
I tried but I didn't completely succeed
function convertToText(obj) {
let string = [];
if (typeof(obj) == "object") {
for (prop in obj) {
if (obj.hasOwnProperty(prop))
string.push(prop "=" convertToText(obj[prop]));
};
return string.join("&");
} else {
string.push(JSON.stringify(obj))
}
return string.join();
}
let obj = {
season: 'winter',
data: {
month: ['December', 'January', 'February']
}
};
console.log(convertToText(obj));
and gets this:
"season='winter'&data=month=0='December'&1='January'&2='February'"
CodePudding user response:
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.
CodePudding user response:
Recursively walk through the entire object hierarchy, keeping track of the path which will be used as prefix for the "key=value" pairs.
Arrays can be handled specially - the key for an array value, as well as any of its indexes need to be wrapped in square brackets.
Finally, the base case would be to convert the prefix to the left side of the equal and a plain value as the right side.
const isPlainObject = data =>
typeof data === "object"
&& data !== null
&& !Array.isArray(data);
const arrayKey = (prefix, index) =>
prefix
//take every element except the last one:
.slice(0, -1)
//append last element and index enclosed in square brackets:
.concat(`[${prefix.at(-1)}]`, `[${index}]`);
function convertToText(data, prefix = []) {
if (isPlainObject(data))
return Object.entries(data)
.map(([key, value]) => convertToText(value, prefix.concat(key)))
.join("&");
if (Array.isArray(data))
return data
.map((x, index) => convertToText(x, arrayKey(prefix, index)))
.join("&")
return `${prefix.join("")}=${data}`;
};
let obj = {
season: 'winter',
data: {
month: ['December', 'January', 'February']
}
};
console.log(convertToText(obj));
CodePudding user response:
Given obj
-
const obj = {
season: 'winter',
data: {
month: ['December', 'January', 'February']
}
}
Start with a generic flat
function to flatten the data -
function *flat(t){
switch (t?.constructor) {
case Object:
case Array:
for (const [k,v] of Object.entries(t))
for (const [path, value] of flat(v))
yield [[k, ...path], value]
break
default:
yield [[], t]
}
}
for (const [path, value] of flat(obj))
console.log(path.join("/"), value)
season winter
data/month/0 December
data/month/1 January
data/month/2 February
Use URLSearchParams to encode the params -
function toSearchParams(t) {
const r = new URLSearchParams()
for (const [path, value] of flat(t))
r.append(
[path[0], ...path.slice(1).map(v => `[${v}]`)].join(""),
value
)
return r
}
console.log(toSearchParams(obj))
URLSearchParams {
'season' => 'winter',
'data[month][0]' => 'December',
'data[month][1]' => 'January',
'data[month][2]' => 'February'
}
URLSearchParams
can be converted to a string -
console.log(String(toSearchParams(obj)))
console.log(decodeURIComponent(String(toSearchParams(obj))))
season=winter&data[month][0]=December&data[month][1]=January&data[month][2]=February
season=winter&data[month][0]=December&data[month][1]=January&data[month][2]=February
See url.searchParams for use with the URL module that you should be leveraging as well.
As a shortcut above, I treat both object keys and array keys as strings. If we want flat
to preserve numeric keys for array values, we can write a different outer loop for each type -
function *flat(t){
switch (t?.constructor) {
case Object:
for (const k of Object.keys(t))
for (const [path, value] of flat(t[k]))
yield [[k, ...path], value] // k: string
break
case Array:
for (let k = 0; k < t.length; k )
for (const [path, value] of flat(t[k]))
yield [[k, ...path], value] // k: number
break
default:
yield [[], t]
}
}
We could collapse the inner loop using an optional path
parameter -
function *flat(t, path = []){
switch (t?.constructor) {
case Object:
for (const k of Object.keys(t))
yield *flat(t[k], [...path, k])
break
case Array:
for (let k = 0; k < t.length; k )
yield *flat(t[k], [...path, k])
break
default:
yield [path, t]
}
}