How to deep merge without overwriting exisiting vales

Within a game im currently developing, I keep all the game/save data in a big JS object.

for example:

const save = {
  inventory: {
    equips: {
      weapon: {...},
      chestplate: {...}
    },
    money: 50123
  },
  gameBooleans: {
    isThisUnlocked: false,
    isThatUnlocked: false
  },
  settings: {
    version: '1.3.4'
  }
}

Whenever I push out a new update, I check to see if the save.settings.version equals to the newest version. If it doesn't, I update the old save data to include the new values.

My problem right now is that as I develop the game, I add new fields to the save data. Eg. save.gameBooleans.isThisNewThingUnlocked: false.

Previously, what I have been doing is manually adding each new field to the object when the game detects an older save:

const currentVersion = '1.3.3'

if (save.settings.version === '1.3.2') {
  
  save.gameBooleans.isThisNewThingUnlocked = false
  save.gameBooleans.isThisOtherNewThingUnlocked = false
  ...etc

  save.settings.version = currentVersion
}

This is becoming quite tedious and I sometimes miss a field which breaks the game and gets my thousands of players upset haha. I was wondering if there was a better approach.

I tried using lodash deepmerge but if I use it like this: _.mergeDeep(upToDateState, oldSave), it doesnt add in the new key/fields. And when used the other way: _.mergeDeep(oldSave, upToDateState), this adds in the new values but overwrites some existing values.

I dont want to write my own function to handle this but its starting to look like I have to. Does anyone have any ideas?


Solution 1:

You can use the Object.keys() function to iterate through eack key of the new object and make a full copy in the save object including the new keys, withoud doing it manually. The function should be like this

function deepCopy(old_, new_) {
  // Iterate through each key of the new object
  Object.keys(new_).forEach(key => {

    //If there is a nested object, recall the function 
    if (typeof new_[key] === 'object' && ! Array.isArray(new_[key]) && new_[key] !== null)
      deepCopy(old_[key], new_[key])

    // If there is a non-object value, just copy the value 
    else
      old_[key] = new_[key];
  });
}

Take into account that this function would make a reference copy of array values (which may be undesirable). However, you can make a new conditional to handle arrays and make a value copy using the spread operator newArray = ...oldArray.

Try it here

I created a new object called update which has 2 new keys in the gameBooleans value:

const save = {
  gameBooleans: {
    isThisUnlocked: false,
    isThatUnlocked: false
  },
  settings: {
    version: '1.3.4'
  }
}

const update = {
  gameBooleans: {
    isThisUnlocked: true,
    isThatUnlocked: true,
    foo: true,
    bar: true,
  },
  settings: {
    version: '1.3.4'
  }
}


function deepCopy(old_, new_) {
  Object.keys(new_).forEach(key => {
    if (typeof new_[key] === 'object' && ! Array.isArray(new_[key]) && new_[key] !== null)
      deepCopy(old_[key], new_[key])
    else
      old_[key] = new_[key];
  });
}

deepCopy(save, update)
console.log(save)