Torsten Müller

Converting GET request data into a JSON object

published Feb 15th, 2020

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:

sample-data.js
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:

  1. Mixture of plain properties and nested object properties (a vs. b[some])
  2. Deeply nested properties
  3. Objects that have multiple different subobjects/properties
  4. 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:

Example RegEx
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:

Split result
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

imperative-split.js
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:

Result 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:

functional-recursive.js
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:

  1. The object containing the accumulated parsing result of the data (acc),
  2. an array with the (potentially) nested key parts and
  3. 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:

Deep nested object
{ "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:

  1. 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.
  2. 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:

Duplicate key
{
  "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:

Duplicate key
{
  "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.