Understanding the 'object' Datatype to transform nested JSON keys

Recently, I got to experience my first (mock) technical interview where I was asked to transform a valid JSON string's keys from kebab-case to camelCase .

Part 1: A Single JSON string

// example input 
const exampleJson = `{
  "first-name": "Brandon",
  "last-name": "Dusch",
  "city-and-state": "North Chatham, MA",
  "zip": "12345-1234"
}`;

// example output
`{
  "firstName": "Brandon",
  "lastName": "Dusch",
  "cityAndState": "North Chatham, MA",
  "zip": "12345-1234"
}`;

Notice that the initial input was a stringified JSON. We'd need to parse the string to turn it into a true JSON object.

let parsedData = JSON.parse(exampleJson)

Because objects in JavaScript are not iterable, we should loop through an array of its keys instead. This allows us to access each key-value pair in a iterable fashion. A for..in loop would be helpful, in this case.

Next, we need to make sure we are honoring the rules of camelCase:

  • the first word in the phrase is not capitalized
  • the remaining words in the phrase are capitalized

We need a test case for this using a combination of an if..else statement and using Array.split and Array.map. Let's enclose everything in a function entitled transformKebabCaseToCamelCase():

// example input 
const exampleJson = `{
  "first-name": "Brandon",
  "last-name": "Dusch",
  "city-and-state": "North Chatham, MA",
  "zip": "12345-1234"
}`;

function transformKebabCaseToCamelCase(jsonData) {
  let transformedJson = {} // initialized as empty object, we'll build as we go
  for(let key in jsonData) {
    let newKeyArray = key.split('-')
    let newKey = newKeyArray.map((word, index) => {
      if(index === 0) {
        return word
      } else {
        return word[0].toUpperCase() + word.slice(1)
      }
    }).join("")
    transformedJson[newKey] = jsonData[key]
  } 
  return JSON.stringify(transformedJson, null, 2)
}

console.log(transformKebabCaseToCamelCase(JSON.parse(exampleJson)))

// expected output
`{
  "firstName": "Brandon",
  "lastName": "Dusch",
  "cityAndState": "North Chatham, MA",
  "zip": "12345"
}`

Excellent! We are now returning a transformed version of our original JSON string. But what if we had key-value pairs where our values are nested collections (such as arrays and objects)?

Part 2: Nested Arrays and Objects

Imagine if we changed out expectations to be something like this:

// example input 
const exampleJson = `[{
  "first-name": "Brandon",
  "last-name": "Dusch",
  "address": {
    "city-and-state": "North Chatham, MA",
    "zip": "12345-1234"
  },
  "pets": [{
     "pet-gender": "male",
     "pet-name": "Bruno"
   },  {
     "pet-gender": "female",
     "pet-name": "Lana-Kane"
   }]
}]`;

// expected output
`[{
  "firstName": "Brandon",
  "lastName": "Dusch",
  "address": {
    "cityAndState": "North Chatham, MA",
    "zip": "12345-1234"
  },
  "pets": [{
     "petGender": "male",
     "petName": "Bruno"
   },  {
     "petGender": "female",
     "petName": "Lana-Kane"
   }]
}]`;

This was part of the interview was challenging. It really tests your comfort level with working with Arrays and Objects. It also requires a good understanding of what JavaScript considers as an object.

JavaScript is a loosely typed, dynamic programming language. The object type is a composite datatype that encapsulates Array, Function and Object (even null is of object type --> typeof null === 'object' // true

Another component to figuring out this part of the problem is understanding how to separate concerns and use recursion. For understandability, we should separate the task of actually converting keys to camelCase to it's own function:

function transformKey(key) {
  let newKeyArray = key.split("-");
  let newKey = newKeyArray
    /// forEach, reduce, filter, find, some
    .map((word, index) => {
      if (index === 0) {
        return word;
      } else {
        return word[0].toUpperCase() + word.slice(1);
      }
    })
    .join("");
  return newKey;
}

However, we now have to consider this: what if the values themselves are objects or arrays that contain more objects? This is where recursion comes in handy.

Let's define another helper function called transformValue(). The base case in this scenario would probably be if the given value is not an Array or an object (not an array or a function), just return the value.

In the case where a given value is an array we should map over each element v such that we recursively call transformValue(v) each time. We'll then return the transformed array.

Finally, if the given value is an object (meaning it's an instance of the class Object but it's not an array or a function), we'll iterate over each key and assigned a newly transformed key, newKey = transformKey(key). Assuming we initialized an empty object like newData = {}, we'll map newData[newKey] to a recursive call to transformValue(), passing in value[key].

// test for a true JSON object is abstracted to a function called "isObject()"
function isObject(data) {
  return (
        obj === Object(obj) && !Array.isArray(obj) && typeof obj !== "function"
  )
}

Our final code will look something like this: 
function transformValue(value) {
  let newData = {}
  if(Array.isArray(value) {
    return value.map((v) => {
      return transformValue(v)
    }
  } else if(isObject(value)) {
    for(let key in value) {
      let newKey = transformKey(key)
      newData[newKey] = transformValue(value[key])
    }
  return newData
  } else {
      // our base case
      return value 
  }
}


function transformKey(key) {
  let newKeyArray = key.split("-")
  let newKey = newKeyArray.map((word, index) => {
    if(index === 0) {
      return word
    } else {
      return word[0].toUpperCase() + word.slice(1)
    }
  }).join("")
  return newKey
}

function transformKebabCaseToCamelCase(data) {
  const parsedData = JSON.parse(data)
  return JSON.stringify(transformValue(pasedData), null, 2)
}

And now when we log it to the console:

// example input 
const exampleJson = `[{
  "first-name": "Brandon",
  "last-name": "Dusch",
  "address": {
    "city-and-state": "NorthChatham, MA",
    "zip": "12345-1234"
  },
  "pets": [{
     "pet-gender": "male",
     "pet-name": "Bruno"
   },  {
     "pet-gender": "female",
     "pet-name": "Lana-Kane"
   }]
}]`

console.log(transformKebabCaseToCamelCase(exampleJson))

// expected output
`[{
  "firstName": "Brandon",
  "lastName": "Dusch",
  "address": {
    "cityAndState": "North Chatham, MA",
    "zip": "12345-1234"
  },
  "pets": [{
    "petsGender": "male",
    "petsName": "Bruno"
   }, {
     "petGender": "female",
     "petName": "Lana-Kane"
   }]
}]`

The combination of recursion, checking for true object instances and separating concerns and tasks into helper functions brings us this newly transformed JSON string.

Conclusion

Among the "musts" of JavaScript skills are Array methods/ manipulation and the ambiguity of the object datatype. This problem is an excellent exercise in those skills along with many others.

Figuring out the rest of the second part of the problem felt truly satisfying. I am excited to tackle even more complex, interesting problems.

Happy Coding!

Resources

developer.mozilla.org/en-US/docs/Web/JavaSc..

medium.com/better-programming/string-case-s..

developer.mozilla.org/en-US/docs/Web/JavaSc..

developer.mozilla.org/en-US/docs/Learn/Java..

No Comments Yet