JSON-Patch icon indicating copy to clipboard operation
JSON-Patch copied to clipboard

Attempting to use if mirror has a toJSON and object does not breaks

Open NoLongerBreathedIn opened this issue 2 years ago • 2 comments

For example, suppose you have the following objects:

const object = {
    bug: new Date('2032-01-04'),
    nobug: new Date('2032-01-04')
};
const mirror = {
    bug: new Date('2032-01-04'),
    nobug: new Date('2032-01-04').toJSON()
};

Then compare(mirror, obj); returns the following array:

[{
    op: "replace",
    path: "/nobug",
    value: "2032-01-04T00:00:00.000Z"
}, {
    op: "add",
    path: "/bug/0",
    value: "2"
}, {
    op: "add",
    path: "/bug/1",
    value: "0"
}, {
    op: "add",
    path: "/bug/2",
    value: "3"
}, {
    op: "add",
    path: "/bug/3",
    value: "2"
}, {
    op: "add",
    path: "/bug/4",
    value: "-"
}, {
    op: "add",
    path: "/bug/5",
    value: "0"
}, {
    op: "add",
    path: "/bug/6",
    value: "1"
}, {
    op: "add",
    path: "/bug/7",
    value: "-"
}, {
    op: "add",
    path: "/bug/8",
    value: "0"
}, {
    op: "add",
    path: "/bug/9",
    value: "4"
}, {
    op: "add",
    path: "/bug/10",
    value: "T"
}, {
    op: "add",
    path: "/bug/11",
    value: "0"
}, {
    op: "add",
    path: "/bug/12",
    value: "0"
}, {
    op: "add",
    path: "/bug/13",
    value: ":"
}, {
    op: "add",
    path: "/bug/14",
    value: "0"
}, {
    op: "add",
    path: "/bug/15",
    value: "0"
}, {
    op: "add",
    path: "/bug/16",
    value: ":"
}, {
    op: "add",
    path: "/bug/17",
    value: "0"
}, {
    op: "add",
    path: "/bug/18",
    value: "0"
}, {
    op: "add",
    path: "/bug/19",
    value: "."
}, {
    op: "add",
    path: "/bug/20",
    value: "0"
}, {
    op: "add",
    path: "/bug/21",
    value: "0"
}, {
    op: "add",
    path: "/bug/22",
    value: "0"
}, {
    op: "add",
    path: "/bug/23",
    value: "Z"
}]

NoLongerBreathedIn avatar Oct 30 '23 14:10 NoLongerBreathedIn

This issue persists since 2021 which is really bad for such a far reaching library. We have the same issue. It happens when both objects contain a Date. Duplicate of #278

Workaround: when creating the objects to compare, use toIsoString beforehand. e.g. bug = new Date('2032-01-04').toIsoString();

Error Source: // src/duplex.ts line 148 to 149 if (typeof obj.toJSON === "function") { obj = obj.toJSON(); } Explanation:

  • When checking for equality in the _generate function, having a Date object in both mirror and obj will cause line 166 to return true. (since typeof myDate is also object)
  • this means the _generate function will be called with "object.bug" and "mirror.bug"
  • line 148 will convert object to its JSON version -> a string representing new Date('2032-01-04') which is "2032-01-04T00:00:00"
  • however, the mirror object will not be converted to json.
  • this means newKeys will have all the characters of the string in it
  • but oldKeys will be an empty array
  • this causes line 198 to 203 to be reached, resulting in all the "add" operations

Possible Solution:

  • check typeof obj.toJSON === "function" and replacing obj and mirror with the result before the type check in line 166. This would prevent the call to _generate() with "object.bug" and "mirror.bug" and will terminate without recursion.

begrs avatar Oct 31 '23 12:10 begrs

Here's a little helper function which patches Date values - it traverses over arrays and objects:

/**
 * The function takes an input value and applies data type patches if the input value is:
 * - `Date` --> JSON representation as fast-json-patch cannot handle Dates
 * - an `object` --> traverses the object tree applying patches to properties where necessary
 * - an `array` --> traverses the array elements applying patches to properties where necessary
 *
 * All other data types - e.g. `string`, `number` will stay unpatched.
 *
 * @returns patched input when necessary
 */
export function patchDataTypes<T>(input: T): any {
  if (input instanceof Date) {
    return input.toJSON() as string;
  } else if (typeof input === 'object' && input !== null) {
    if (Array.isArray(input)) {
      // If it's an array, recursively convert each element
      return input.map(patchDataTypes);
    } else {
      // If it's an object, recursively convert each property
      const result: Record<string, unknown> = {};
      for (const key in input) {
        result[key] = patchDataTypes(input[key]);
      }
      return result;
    }
  } else {
    return input;
  }
}

bohoffi avatar Dec 14 '23 12:12 bohoffi