javascriptjsongoogle-apps-scriptcompare

How to Compare nested objects for changes and create a report of changes


I have two JSON files that come from an app.

I need to compare them for changes and create a report of the changes.

I can mostly do this, but I am having trouble creating a report of the changed nested objects within an object.

I can see that the objects have changed, and I can post back both entire objects. My issue is that some of them can have dozens or even hundreds of key-value pairs, and it is difficult to find which entries of the objects were changed.

How do you compare two JSON files with the desired report structure (shown below) for matching objects with nested objects of two JSON files?

two objects from each file are matched if their ids match.

In my example, there is a single matching pair of objects with a nested options object, sector/sector2, but multiple matched pairings could exist.

(my apologies for all the code)

I have created a Google sheet with all the code shown for convenience https://docs.google.com/spreadsheets/d/1G2c1qsBvW5rW9Z0yRbF6OhBkw8cZ7U5XHY0Ab0u2z-M/edit?usp=sharing

Thanks

let oldJSON = [
 {
  "id": "c3c6f410f58e5836431b473ebcf134756232d04f2bf35edff8",
  "label": "Sector2",
  "options": {
   "62f92fab79ac81d933765bd0bbc4a1f5ea26cb3a088bcb4e6e": {
    "index": 0,
    "value": "Bob",
    "label": "Bob"
   },
   "2fe91aa3567c0d04c521dcd2fc7e40d7622bb8c3f594d503da": {
    "index": 1,
    "value": "Student",
    "label": "Student"
   },
   "c59ea1159f33b91a7f6edc6925be5e373fc543e4": {
    "index": 2,
    "value": "BBB",
    "label": "BBB"
   },
   "c59ea1159f33b91a7f6edc6925be5e373fc54AAA": {
    "index": 3,
    "value": "Orange Duck",
    "label": "Orange Duck"
   }
  }
 },
 {
  "id": "f794c6a52e793ee6f5c42cd5df6b4435236e3495e951709485",
  "label": "Brown Cow"
 },
 {
  "id": "f794c6a52e793ee6f5c42cd5df6b4435236e3495e95170ZZZ",
  "label": "Red Fish"
 },
];

 let newJSON = [
 {
  "id": "f794c6a52e793ee6f5c42cd5df6b4435236e3495e951709485",
  "label": "Green Frog"
 },
 {
  "id": "c3c6f410f58e5836431b473ebcf134756232d04f2bf35edff8",
  "label": "Sector",
  "options": {
   "62f92fab79ac81d933765bd0bbc4a1f5ea26cb3a088bcb4e6e": {
    "index": 0,
    "value": "Bob",
    "label": "Bob"
   },
   "c59ea1159f33b91a7f6edc6925be5e373DDDDDDDDD": {
    "index": 1,
    "value": "GrassLand",
    "label": "TreeLand"
   },
   "2fe91aa3567c0d04c521dcd2fc7e40d7622bb8c3f594d503da": {
    "index": 2,
    "value": "Student",
    "label": "Student"
   },
   "c59ea1159f33b91a7f6edc6925be5e373fc543e4": {
    "index": 3,
    "value": "AAA",
    "label": "AAA"
   }
  }
 },
 {
  "id": "f794c6a52e793ee6f5c42cd5df6b4435236e3495e951709AAA",
  "label": "Blue Bird"
 }
];

My code so far

  // Create an object to store the results of the comparison.
  const results = {};
  
  // Loop through the first array.
  for (const item1 of array1) {
    // Check if the item exists in the second array.
    let item2 = array2.find(item => item.id === item1.id);
    
    // If the item does not exist in the second array, add it to the results object as a deleted item.
    if (!item2) {
      results.deleted = results.deleted || [];
      results.deleted.push(item1);

    } else {
      // If the item exists in the second array, compare the two objects.
      const differences = compareObjects(item1, item2);
      
      // If there are any differences, add the item to the results object as a changed item.
      if (Object.keys(differences).length > 0) {
        
        results.changed = results.changed || [];
       
        let
          label1 = item1.label,
          label2 = item2.label;

        results.changed.push({
          label1,
          label2,
          differences,
        });
      }
    }
  }

  // Loop through the second array.
  for (const item2 of array2) {
    // Check if the item exists in the first array.
    const item1 = array1.find(item => item.id === item2.id);

    // If the item does not exist in the first array, add it to the results object as an added item.
    if (!item1) {
      results.added = results.added || [];
      results.added.push(item2);
    }
  }

  // Return the results object.
  return results;
}

function compareObjects(object1, object2) {
  // Create an object to store the differences between the two objects.
  const differences = {};

  // Loop through the keys of the first object.
  for (const key of Object.keys(object1)) {
    // Get the value of the key in the second object.
    const value2 = object2[key];

    // If the value does not exist in the second object, add it to the differences object as a deleted value.
    if (value2 === undefined) {
      differences[key] = {
        value1: object1[key],
        value2: undefined,
      };
    } else {
      // If the value exists in the second object, compare the two values.
      if (object1[key] !== value2) {
        differences[key] = {
          value1: object1[key],
          value2,
        };
      }
    }
  }

  // Loop through the keys of the second object.
  for (const key of Object.keys(object2)) {
    // Check if the key exists in the first object.
    if (!object1.hasOwnProperty(key)) {
      // If the key does not exist in the first object, add it to the differences object as an added value.
      differences[key] = {
        value1: undefined,
        value2: object2[key],
      };
    }
  }

  // Return the differences object.
  return differences;
}

The output I get

{
 "changed": [
  {
   "label1": "Sector2",
   "label2": "Sector",
   "differences": {
    "label": {
     "value1": "Sector2",
     "value2": "Sector"
    },
    "options": {
     "value1": {
      "62f92fab79ac81d933765bd0bbc4a1f5ea26cb3a088bcb4e6e": {
       "index": 0,
       "value": "Bob",
       "label": "Bob"
      },
      "2fe91aa3567c0d04c521dcd2fc7e40d7622bb8c3f594d503da": {
       "index": 1,
       "value": "Student",
       "label": "Student"
      },
      "c59ea1159f33b91a7f6edc6925be5e373fc543e4": {
       "index": 2,
       "value": "BBB",
       "label": "BBB"
      },
      "c59ea1159f33b91a7f6edc6925be5e373fc54AAA": {
       "index": 3,
       "value": "Orange Duck",
       "label": "Orange Duck"
      }
     },
     "value2": {
      "62f92fab79ac81d933765bd0bbc4a1f5ea26cb3a088bcb4e6e": {
       "index": 0,
       "value": "Bob",
       "label": "Bob"
      },
      "c59ea1159f33b91a7f6edc6925be5e373DDDDDDDDD": {
       "index": 1,
       "value": "GrassLand",
       "label": "TreeLand"
      },
      "2fe91aa3567c0d04c521dcd2fc7e40d7622bb8c3f594d503da": {
       "index": 2,
       "value": "Student",
       "label": "Student"
      },
      "c59ea1159f33b91a7f6edc6925be5e373fc543e4": {
       "index": 3,
       "value": "AAA",
       "label": "AAA"
      }
     }
    }
   }
  },
  {
   "label1": "Brown Cow",
   "label2": "Green Frog",
   "differences": {
    "label": {
     "value1": "Brown Cow",
     "value2": "Green Frog"
    }
   }
  }
 ],
 "deleted": [
  {
   "id": "f794c6a52e793ee6f5c42cd5df6b4435236e3495e95170ZZZ",
   "label": "Red Fish"
  }
 ],
 "added": [
  {
   "id": "f794c6a52e793ee6f5c42cd5df6b4435236e3495e951709AAA",
   "label": "Blue Bird"
  }
 ]
}

The output desired or something equivalent

{
 "changed": [
  {
   "label1": "Sector2",
   "label2": "Sector",
   "differences": {
    "label": {
     "value1": "Sector2",
     "value2": "Sector"
    },
    "options": {
     "value1": {
      "c59ea1159f33b91a7f6edc6925be5e373fc54AAA": {
       "deleted": {
       "index": 3,
       "value": "Orange Duck",
       "label": "Orange Duck"
      }
      }
     },
     "value2": {
      "added":{
      "c59ea1159f33b91a7f6edc6925be5e373DDDDDDDDD": {
       "index2": 1,
       "value": "GrassLand",
       "label": "TreeLand"
       }
      },
      "2fe91aa3567c0d04c521dcd2fc7e40d7622bb8c3f594d503da": {
       "changed": {
       "index1": 1,
       "index2": 2,
       "value1": "Student",
       "value2": "Student",
       "label1": "Student",
       "label2": "Student"
       }
      },
      "c59ea1159f33b91a7f6edc6925be5e373fc543e4": {
       "changed": {
       "index1": 2,
       "index2": 3,
       "value1": "BBB",
       "value2": "AAA",
       "label1": "BBB",
       "label2": "AAA"
       }
      }
     }
    }
   }
  },
  {
   "label1": "Brown Cow",
   "label2": "Green Frog",
   "differences": {
    "label": {
     "value1": "Brown Cow",
     "value2": "Green Frog"
    }
   }
  }
 ],
 "deleted": [
  {
   "id": "f794c6a52e793ee6f5c42cd5df6b4435236e3495e95170ZZZ",
   "label": "Red Fish"
  }
 ],
 "added": [
  {
   "id": "f794c6a52e793ee6f5c42cd5df6b4435236e3495e951709AAA",
   "label": "Blue Bird"
  }
 ]
}

Solution

  • This gives a report that captures the spirit of what you asked. output is slightly different

    function compareJSONs(oldJSON,newJSON) {
    
      // Create an object to store the results of the comparison
      let
        results = {
                   deleted: [],
                   added: [],
                   changed: []
                  };
    
      // Loop through the old JSON array
      for (const oldObj of oldJSON) {
        // Find the matching object in the new JSON array
        const newObj = newJSON.find(newItem => newItem.id === oldObj.id);
    
        if (!newObj) {
          // Object exists in oldJSON but not in newJSON (deleted)
          results.deleted.push(oldObj);
        } 
        else {
          // Compare nested options
          let changedOptions = {};
    
          for (const key in oldObj.options) {
            if (newObj.options[key] !== oldObj.options[key]) {
              changedOptions[key] = {
                oldValue: oldObj.options[key],
                newValue: newObj.options[key]
              };
            };
          };
    
          for (const key in newObj.options) {
            //console.log(newObj.options[key])
            if (newObj.options[key] !== oldObj.options[key]) {
              changedOptions[key] = {
                oldValue: oldObj.options[key],
                newValue: newObj.options[key]
              };
            };
          };
        
          //Remove objects where keys have identical value (all the NON-changed options)
          for (const key in changedOptions) {
            const {oldValue: a, newValue: b} = changedOptions[key];
            a?.index === b?.index && a?.value === b?.value && a?.label === b?.label && delete changedOptions[key];
          };
    
        
          if (Object.keys(changedOptions).length > 0) {
            // Object exists in both oldJSON and newJSON, with changes
            results.changed.push({
                id: newObj.id,
                oldLabel: oldObj.label,
                newLabel: newObj.label,
                changedOptions
            });
          };
        };
      }; 
    
      // Check for added objects
      for (const newObj of newJSON) {
        if (!oldJSON.find(oldItem => oldItem.id === newObj.id)) {
          results.added.push(newObj);
        };
      };
    
      return results;
    }
    

    EDIT:

    If you want the structure you posted, then use the following after compareJSONs

    function mergOBJ(data) {
      const transformedData = { ...data }; // Create a copy to avoid mutation
    
      transformedData.ChangedObjects.changedOptions = Object.entries(data.ChangedObjects.changedOptions).reduce((acc, [key, value]) => {
         console.log(value.oldValue)
    
        if(value.oldValue!==undefined && value.newValue!==undefined){
        acc[key] = {
          optionChanged: {
            oldIndex: value.oldValue.index,
            newIndex: value.newValue.index,
            oldValue: value.oldValue.value,
            newValue: value.newValue.value,
            oldLabel: value.oldValue.label,
            newLabel: value.newValue.label,
          },
         }
        }else if(value.oldValue===undefined && value.newValue) {
          acc[key] = {
          optionAdded: {
            index: value.newValue.index,
            value: value.newValue.value,
            label: value.newValue.label,
          },
         }
        }else if(value.oldValue && value.newValue===undefined) {
          acc[key] = {
          optionDeleted: {
            index: value.oldValue.index,
            value: value.oldValue.value,
            label: value.oldValue.label,
          },
         }
        };
        return acc;
      }, {});
      return transformedData;
    }
    

    Use like:

    let Changed = {"ChangedObjects":results.changed[0]}

    console.log( JSON.stringify(Changed, null,1))

    const mergedReport = mergOBJ(Changed);

    console.log(JSON.stringify(mergedReport, null, 1));

    {
     "ChangedObjects": {
      "id": "c3c6f410f58e5836431b473ebcf134756232d04f2bf35edff8",
      "oldLabel": "Sector2",
      "newLabel": "Sector",
      "changedOptions": {
       "2fe91aa3567c0d04c521dcd2fc7e40d7622bb8c3f594d503da": {
        "optionChanged": {
         "oldIndex": 1,
         "newIndex": 2,
         "oldValue": "Student",
         "newValue": "Student",
         "oldLabel": "Student",
         "newLabel": "Student"
        }
       },
       "c59ea1159f33b91a7f6edc6925be5e373fc543e4": {
        "optionChanged": {
         "oldIndex": 2,
         "newIndex": 3,
         "oldValue": "BBB",
         "newValue": "AAA",
         "oldLabel": "BBB",
         "newLabel": "AAA"
        }
       },
       "c59ea1159f33b91a7f6edc6925be5e373fc54AAA": {
        "optionDeleted": {
         "index": 3,
         "value": "Orange Duck",
         "label": "Orange Duck"
        }
       },
       "c59ea1159f33b91a7f6edc6925be5e373DDDDDDDDD": {
        "optionAdded": {
         "index": 1,
         "value": "GrassLand",
         "label": "TreeLand"
        }
       }
      }
     }
    }