Converting GET request data into a JSON object
On a recent project, form data was sent via a GET HTTP request to a Node backend. The same data was also sent from another API via a JSON object, so we needed to convert the GET data into JSON. This post describes two ways to accomplish that, one procedural and one using recursion. This is an example of the data object we’re trying to convert to JSON:
let testObject = {
a: 'some string',
'b[some][label]': 42,
'b[some][test]': 'some other string',
'b[yet][another]': 'one',
'c[do_something][or_other][and][more]': 'yes',
'd[some][label]': 42,
'd[some][label]': 26,
};
This object covers most of the data “problems” we’re going to encounter:
- Mixture of plain properties and nested object properties (
a
vs.b[some]
) - Deeply nested properties
- Objects that have multiple different subobjects/properties
- Two parameters with the same path
Common code for both solutions
One of the main tasks to accomplish is to determine the parameter names in order to create corresponding object properties. This can be done through parsing the key name using a regular expression (RegEx) such as the following:
let partsRegEx = /\[?(\w+)/g;
This RegEx starts capturing all patterns (because of the g
flag) that contain a sequence of letters
and is optionally preceded by an opening square bracket. So from a string representing a key
such as the following, we get an array containing the following matches from the expression:
let key = 'b[some][key]';
let parsedKeys = [ 'b', '[some', '[key' ];
The alternative, and probably more efficient and faster, is using the JavaScript split()
function:
let key = 'b[some][key]';
let parsedKeys = key.split('['); // Returns [ 'b', 'some]', 'key]' ];
Imperative implementation
let targetObj = {};
for (a in testObject) {
if (a.indexOf('[') < 0) targetObj[a] = testObject[a];
else {
let parts = a.match(partsRegEx);
let current = targetObj;
for (let c=0; c < parts.length; c++) {
let key = (parts[c].substr(0,1) === '[') ? parts[c].substr(1) : parts[c];
if (!current[key]) {
if (c === parts.length - 1) current[key] = testObject[a];
else current[key] = {};
}
current = current[key];
}
}
}
In this implementation, we instantiate an empty object and then use a for
loop to iterate
over the testObject
properties in the object shown in the first listing. If the parsed key
does not include any opening brace, we have a straight, not nested property which lets us assign
the corresponding property of testObject
to the targetObject
(line 3).
On line 5, we use the afrementioned regular expression to extract the keys and then loop over
them in line 7 using a for
loop. We define another variable called current
on line 6, which
will contain a reference to the current node of targetObj
being considered. That reference is
updated as we traverse through the source object.
Since the keys matched by the RegEx will contain the opening brace, we clip that brace off on
line 8, if present. On the following three lines, if the current key does not already
exists at that level in the object, we either create a new empty node (object), or we assign the
value, if we finished traversing the object key list, as indicated by the condition
c === parths.length - 1
.
As a final step, we advance one level deeper into the object we’re populating, assigning a
reference to the current node to the variable current
. The end result
of this code with the data shown at the beginning of this post looks like the following JSON
object:
{
"a": "some string",
"b": {
"some": {
"label": 42,
"test": "some other string"
},
"yet": {
"another": "one"
}
},
"c": {
"do_something": {
"or_other": {
"and": {
"more": 'yes'
}
}
}
},
"d": {
"some": {
"label": 26
}
}
}
Implementation using Recursion
The same result can also be achieved using a recursive implementation using three functions that work together as shown in this code example:
function setNestedObjectProperty(targetObj, keySegmentList, propValue) {
if (keySegmentList.length === 0) return propValue;
else {
let key = cleanedKey(keySegmentList[0]);
targetObj[key] = setNestedObjectProperty(targetObj[key] || {}, keySegmentList.slice(1), propValue)
return targetObj;
}
}
const cleanedKey = (rawKey) => {
return (rawKey.substr(0,1) === '[') ? rawKey.substr(1) : rawKey;
};
const mapStringToObjectKey = (acc, objectKey) => {
return setNestedObjectProperty(acc, objectKey.match(partsRegEx), testObject[objectKey]);
};
let redVal = Object.keys(testObject).reduce(mapStringToObjectKey , {});
The processing of this implementation starts on line 18, after the definition of three functions
implementing the functionality: In line 18, we extract the keys of the
original object and run a reduce
on the resulting array. The function passed to reduce()
calls
the setNestedObjectProperty()
method for each key found in the object with three parameters:
- The object containing the accumulated parsing result of the data (
acc
), - an array with the (potentially) nested key parts and
- the value to be assigned once the nested property is reached.
The setNestedObjectProperty()
function is recursive to handle objects with deeply nested
structures, such as this object, already seen in the very first listing of this post:
{ "c[do_something][or_other][and][more]": "yes" }
The “real work” takes place in setNestedObjectProperty()
, which either returns the passed value
for this object path or recurses one level deeper into the passed object, creating new object
instances as needed.
Discussion
Both solutions create the same result, and which one someone uses is largely a matter of preference. With that said, let me try to make a case for the second, recursive implementation:
- The iteration over the different object keys (as strings) is separated from the recursive processing of each individual key. This separation could also be implemented in the iterative approach, of course, but due to the recursive implementation, it follows more naturally in the second implementation.
- The recursive implementation consists of mostly pure functions, which means writing tests
for this implementation is going to be easy, in particular for special cases for the
setNestedObjectProperty()
method.
There is also one ambiguity in this implementation/requirements. The parameter d
has the same
path twice:
{
"d[some][label]": 42,
"d[some][label]": 26
}
In the current implementation, this construct overwrites the first value with the second one (same string index on the object), so that the last value is the only one available. If we wanted to create arrays of values, we’d have to specify the keys as follows:
{
"d[some][label][0]": 42,
"d[some][label][1]": 26
}
The numbered array indices are required in this notation, because without the numbered indices, the keys of the object would be identical and thus overwrite each other, just as in the previous example. This syntax is currently not considered in the implementation and not interpreted as arrays.