Affected versions: confirmed on 3.3.0, still present in 4.107.0 (method is byte-for-byte identical).
Body:
DeepEquals.deepEquals(a, b) throws an NPE while generating the breadcrumb diff (only on the not-equal path) when the object graph contains a type whose custom equals() returns false for two instances that are nevertheless deeply field-equal.
Root cause is in getContainingDescription:
private static String getContainingDescription(List<ItemsToCompare> path) {
ListIterator<ItemsToCompare> it = path.listIterator(path.size());
String a = it.previous().difference.getDescription(); // <-- last node's `difference` not null-checked
if (it.hasPrevious()) {
Difference diff = it.previous().difference;
if (diff != null) { // parent node IS null-checked
String b = diff.getDescription();
if (b != null) {
return b;
}
}
}
return a;
}
The last node's difference is dereferenced without a null-check, unlike the parent node just below it. It becomes null via the custom-equals branch in the recursive comparison: when key1.equals(key2) is false but the recursive field-by-field deep comparison finds them equal (diff_item == null), nothing is pushed onto the stack, so the root node (new ItemsToCompare(a, b), difference == null) stays on top → NPE.
Minimal reproduction:
class IdentityEquals {
final String value = "x";
@Override public boolean equals(Object o) { return this == o; }
@Override public int hashCode() { return System.identityHashCode(this); }
}
DeepEquals.deepEquals(new IdentityEquals(), new IdentityEquals()); // throws NPE
Suggested fix: null-guard the last node the same way the parent node is guarded, e.g. String a = last.difference != null ? last.difference.getDescription() : null; (and fall back gracefully when both are null).
Affected versions: confirmed on 3.3.0, still present in 4.107.0 (method is byte-for-byte identical).
Body:
DeepEquals.deepEquals(a, b)throws an NPE while generating the breadcrumb diff (only on the not-equal path) when the object graph contains a type whose customequals()returnsfalsefor two instances that are nevertheless deeply field-equal.Root cause is in
getContainingDescription:The last node's
differenceis dereferenced without a null-check, unlike the parent node just below it. It becomesnullvia the custom-equals branch in the recursive comparison: whenkey1.equals(key2)isfalsebut the recursive field-by-field deep comparison finds them equal (diff_item == null), nothing is pushed onto the stack, so the root node (new ItemsToCompare(a, b),difference == null) stays on top → NPE.Minimal reproduction:
Suggested fix: null-guard the last node the same way the parent node is guarded, e.g.
String a = last.difference != null ? last.difference.getDescription() : null;(and fall back gracefully when both are null).