1

I am trying to create a function that takes a target json object a source json object (the update) The rule is that the target must be updated by the source. If the key are the same, the value of the target is overwritten. If the key contains a dot . we consider it as a nested update (example 3) If the update key does not exist in the target, we create it.

Example 1:

var target = {
    foo: 1,
}
var source = {
    foo: 22,
}
var output = {
    foo: 22
}

Example 2:

var target = {
    foo: {
        bar: 1
    },
}
var source = {
    foo: {
        baz: 999
    },
}
var output = {
    foo: {
        baz: 999
    }
}

Example 3:

var target = {
    foo: {
        bar: 1,
        baz: 99,
    },
}
var source = {
    'foo.bar': 22,
}
var output = {
    foo: {
        bar: 22,
        baz: 99
    }
}

Example 4 (Array)

We assume that numbers are invalid key for the source. For example, the below source will never occur

// this will never happen
let source = {
        1: 'something'
}

However, update can update the value of an array at a specif index using the dot . method

const source = {
    foo: [0, 1,2,3, {four: 4}]
}
const target = {
    'foo.0': 'zero',
    'foo.4.four': 'four',
    'foo.4.five': 5,
}
const output = {
    foo: ['zero', 1, 2, 3, {four: 'four', five: 5}]
}

Hence array can not change in size using the dot update, to change the size, we need to overwrite the entire array

My function below works perfectly fine with the Example 1 but I cannot get it to work with example 2 and 3.

Instead I obtain:

var example2 = {
    bar: 1,
    baz: 999,
}


var example3 = {
    foo: {
        bar: 1,
        baz: 99,
    },
    'foo.bar': 22,
}

function deepMerge(target, source) {
    Object.entries(source).forEach(([key, value]) => {
        if (value && typeof value === 'object') {
            deepMerge(target[key] = target[key] || {}, value);
            return;
        }
        target[key] = value;
    });
    return target;
}
7
  • What happened to bar in example #2? Commented Aug 3, 2020 at 9:45
  • It is overwritten by the the entire value of key foo in the source because they have exact same key Commented Aug 3, 2020 at 9:50
  • Right, so the only time it should look deeper is if the source has dotted keys, correct? Commented Aug 3, 2020 at 9:51
  • Yes! @Phil I couldn't get that to work :( Commented Aug 3, 2020 at 9:52
  • What about arrays? What should happen if there's a key in source like foo.bar[0].baz? Commented Aug 3, 2020 at 9:56

3 Answers 3

1

I would perform the following actions

  1. Iterate all source entries (key / value pairs)
  2. Split the key into an array, separate by .. For non path-like keys, this will just be the key itself
  3. Find or create inner objects by path and then assign the new value

// utility function
const isObjectOrArray = obj => typeof obj === 'object' && obj !== null

const deepMerge = (target, source) => {
  // create a shallow copy because who wants to mutate source data
  const result = { ...target }
  
  // Iterate source entries
  Object.entries(source).forEach(([ path, val ]) => {
    const segments = path.split('.')
    // save the last segment to assign the value
    const last = segments.pop()
    
    // Find or create the deepest object by path
    const deepest = segments.reduce((obj, segment) => {
      // create objects if they aren't already
      if (!isObjectOrArray(obj[segment])) {
        // perhaps check if segment is a number and create an array instead
        // ¯\_(ツ)_/¯ I'm tired
        obj[segment] = {}
      }
      return obj[segment]
    }, result)
    
    // write the new value
    deepest[last] = val
  })
  return result
}

var target = {
  abc: 'abc',
  bar: {
    baz: 1
  },
  foo: {
    bar: 1,
    baz: 99,
  },
  nums: [0, 1,2,3, {four: 4}]
}
var source = {
  'foo.bar': 22,
  abc: 'def',
  'nums.0': 'zero',
  'nums.4.four': 'four',
  'nums.4.five': 5,
  bar: {
    bar: 'bar!'
  },
  'brand.new.object': 'value'
}

const output = deepMerge(target, source)
console.info(output)
.as-console-wrapper {max-height: none !important; top: 0;}

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

1 Comment

this is brilliant
1

This should works for every example you provided:

function deepMerge(target: any, source: any) {
    Object.keys(source).forEach((key: string) => {
        if (key.includes('.')) {
            const nested: any = {}
            nested[key.split('.')[1]] = source[key]
            target[key.split('.')[0]] = deepMerge(target[key.split('.')[0]], nested)
            delete source[key]
        }
    });
    return { ...target, ...source }
}

1 Comment

This unfortunately doesn't work for the arrays (see OP's example #4)
0

Here if you break {'foo.bar': 22} into { foo: {bar: 22}} before giving it to input as a source you should get expected result.

function breakItApart(obj) {
  Object.entries(obj).forEach(([key, value]) => {

        [firstKey, anotherKey] = key.split('.')
        if (anotherKey) {
            obj[firstKey] = obj[firstKey] || {};
            obj[firstKey][anotherKey] = value;
            delete obj[key];
        }
    });
    return obj; 
}

This depends on the constraint that you would only get one dot in a key but you should be able extend this method for multiple dots.

var target = {
    foo: {
        bar: 1,
        baz: 99,
    },
}
var source = {
    'foo.bar': 22,
}

const result = deepMerge(target, breakItApart(source));
console.log(result); 
>> { foo: { bar: 22, baz: 99 } }

Comments

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.