1

I am trying to add and remove values from an array of NESTED json object. Deletion was easy but I am struggling with addition:

export const addNestedJson = (
  json: Record<string, any> | any[],
  value: any,
  keys: string[]
): Record<string, any> | any[] => {
  // Please pass in mutated Values before calling the function

  const isJSONArray = Array.isArray(json);
  if (keys.length === 0) {
    if (isJSONArray) {
      return [...json, value];
    }
    return json;
  }
  const key = keys.shift() as string;
  const index = key ?? 0;
  const indexKey = isNaN(+index) ? 0 : +index;
  const newJson = isJSONArray ? json[indexKey] : json[key];
  const nestedJsonValue = addNestedJson(newJson, value, keys);
  return isJSONArray
    ? [...json, nestedJsonValue]
    : { ...json, [key]: nestedJsonValue };
}

I am trying to use recursion as that seems to be the easiest solution, but for some reason I am not able to visualize and formulate the merge logic.

Here's what I expect:

JSON: [], key: [], valueTobeAdded: 1 should return [1]

JSON: [{a: {b:[]}}], key: ['a','b'], valueTobeAdded: 1 should return [{a:{b:[1]}}]

JSON: [{a: {b:[{c:[4]}]}}], key: ['a','b','0','c'], valueTobeAdded: 1 should return [{a: {b:[{c:[4,1]}]}}]

9
  • NB: What you call JSON is not all JSON. These are objects/arrays, and apparently even instances of a specific class Record. So just drop the term JSON from your code and variable names. JSON is a text format for transferring data. Commented Apr 13, 2022 at 14:48
  • Record is a built-in type :x Commented Apr 13, 2022 at 14:58
  • 2
    In the second example, why would that have to produce what you say? The top level is an array, and a is not (yet) defined on that array... Commented Apr 13, 2022 at 15:18
  • 1
    @pilchard, I think the Asker wants a different behaviour than lodash set, as they want a value to be appended to an array when the end of the path is an array (instead of replacing that array with the new value). Commented Apr 13, 2022 at 15:45
  • 1
    Then it would just be a _.get with a push. _.get(object, keys).push(value); Commented Apr 13, 2022 at 15:53

2 Answers 2

2

First of all, I think that the last two examples of expected output do not define a correct key. Since the inputs are arrays at their top level, and you expect the first array value to be replaced, you should have 0 as the first entry in the key.

In the code I see two issues and have a few more remarks:

  1. In case keys is the empty array, and the data is not an array, it should be replaced by the new value, so instead of return json, you should return value

  2. When coming back from the recursive call, and the current level is an array, the new value should not be appended to the array, but instead be the replacement value for what was at indexKey.

  3. Not a real problem, but I would not mutate keys.

  4. Not a problem with the algorithm, but the name json in your property and variable names is not appropriate. JSON is something you would have to pass to JSON.parse. Anything else should not be called JSON.

This results in the following adapted code:

const addNested = (
  data: Record<string, any> | any[],
  value: any,
  keys: string[]
): Record<string, any> | any[] => {
  const isArray = Array.isArray(data);
  if (keys.length === 0) {
    if (isArray) {
      return [...data, value];
    }
    return value; // Overwrite any data
  }
  // Don't mutate keys with shift
  const key = keys[0] as string;
  const indexKey = +key || 0;
  const newData = isArray ? data[indexKey] : data[key];
  const nestedValue = addNested(newData, value, keys.slice(1));
  return isArray
    ? Object.assign([], data, { [indexKey]: nestedValue }) // Replace at index
    : { ...data, [key]: nestedValue };
}

const addNested = (
  data /*: Record<string, any> | any[] */,
  value /*: any */,
  keys /*: string[] */
) /*: Record<string, any> | any[] */ => {
  const isArray = Array.isArray(data);
  if (keys.length === 0) {
    if (isArray) {
      return [...data, value];
    }
    return value; // Overwrite any data
  }
  // Don't mutate keys with shift
  const key = keys[0] /* as string */;
  const indexKey = +key || 0;
  const newData = isArray ? data[indexKey] : data[key];
  const nestedValue = addNested(newData, value, keys.slice(1));
  return isArray
    ? Object.assign([], data, { [indexKey]: nestedValue }) // Replace at index
    : { ...data, [key]: nestedValue };
}

console.log(addNested([], 1, [])); //  [1]
console.log(addNested([{a: {b:[]}}], 1, [0,'a','b'])); // [{a:{b:[1]}}]
console.log(addNested([{a: {b:[{c:[4]}]}}], 1, [0,'a','b','0','c'])); // [{a: {b:[{c:[4,1]}]}}]

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks a ton for the detailed explanation! This really helped a lot
1

Couple of advices when working with algorithms:

  1. Recursion is actually quite hard for a human brain. Although I agree that some tasks are easier if you know how to apply recursion. Especially in case of nested recursion.
  2. Naming is really important.

So for example, for me, "keys" is quite confusing. If I revisit this function later, I would probably think at first that I should add the value to all of these keys. So I would rather rename the variable into smth like "keyPath".

Now, I'll try to explain how I would solve it. This is probably not the best solution, just something from the top of my head.

So, imagine we have a nested object:

{
    key1: {
        key2: {
            key3: []
        }
    }
}

And then, let's imagine the simplest scenario, we need to insert a value all the way to key3.

In this case, obviously keyPath=["key1", "key2", "key3"]

Now, in order to reach the last key, we can start with first key of the path, and try to go down into the object until we reach the end of keyPath.

  1. take first element of keyPath. const key = "key1";
  2. go deeper into our object: const nestedObj = obj[key];
  3. repeat, but now use nestedObj instead of obj, and "key2", and so forth

This can be a recursion, but I think it's a simple loop, isn't it?

for (const i=0; i<keyPath.length; i++) {
   const key = keyPath[i];
   const newObj = obj[key];
   obj = newObj;
}

This code is a bit too verbose though. We can simplify it like this:

for (const key of keyPath) {
    obj = obj[key];
}

And voila, we got to the final object where we can insert the element:

obj.push(value);

So the whole function would look like this:

function addNested(obj: any, keyPath: (string|number)[], value: any) {
    for (const key of keyPath)
        obj = obj[key];
    obj.push(value);
}

This will already satisfy all of the test cases you had, but as I understood your code, there is one more possibility: last key in the keyPath doesn't exist or is a primitive value (i.e. not an object and not an array).

Well, when we loop into the object, if the key doesn't exist, our obj[key] will return undefined. And we can also check if it contains a primitive value or null.

So in this case, we know that we reached the end of the object, and we simply need to assign our value.

if (typeof obj[key] === 'undefined' || obj[key] === null || obj[key] !== Object(obj[key])) {
    obj[key] = value;
    return;
}

typeof obj[key] === 'undefined' || obj[key] === null can be simplified into obj[key] == null (notice it's == and not ===).

So the function becomes something like this:

function addNested(obj: any, keyPath: (string|number)[], value: any) {
    for (const key of keyPath) {
        if (obj[key] == null || obj[key] !== Object(obj[key])) {
            obj[key] = value;
            return;
        }
        obj = obj[key];
    }
    obj.push(value);
}

This is still not ideal algorithm, because it doesn't do any error checking. For example, if more than 1 key from the keyPath doesn't exist in the object, or keyPath ends with an object, it doesn't handle that.

Still, I hope that this example will help you to improve approach to working with algorithms :)

1 Comment

Thank you very much this was indeed extremely helpful! :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.