google/guava · error · AssertionError

Return value of %s reserialized to an unequal value

Error message

Return value of %s reserialized to an unequal value

What it means

Thrown by testEqualsAndSerializable() when reserializeAndAssert(instance) succeeds in deserialization but the round-tripped object is NOT equal to the original (AssertionFailedError from reserializeAndAssert). This catches broken equals/hashCode with respect to serialization: the object's identity/equality depends on data lost or changed during serialization.

Source

Thrown at android/guava-testlib/src/com/google/common/testing/ClassSanityTester.java:547

     */
    @CanIgnoreReturnValue
    @SuppressWarnings("CatchingUnchecked") // sneaky checked exception
    public FactoryMethodReturnValueTester testEqualsAndSerializable() throws Exception {
      for (Invokable<?, ?> factory : getFactoriesToTest()) {
        try {
          testEqualsUsing(factory);
        } catch (FactoryMethodReturnsNullException e) {
          // If the factory returns null, we just skip it.
        }
        Object instance = instantiate(factory);
        if (instance != null) {
          try {
            reserializeAndAssert(instance);
          } catch (Exception e) { // sneaky checked exception
            throw new AssertionError(
                "Serialization failed on return value of " + factory, e.getCause());
          } catch (AssertionFailedError e) {
            throw new AssertionError(
                "Return value of " + factory + " reserialized to an unequal value", e);
          }
        }
      }
      return this;
    }

    private ImmutableList<Invokable<?, ?>> getFactoriesToTest() {
      ImmutableList.Builder<Invokable<?, ?>> builder = ImmutableList.builder();
      for (Invokable<?, ?> factory : factories) {
        if (returnTypeToTest.isAssignableFrom(factory.getReturnType().getRawType())) {
          builder.add(factory);
        }
      }
      ImmutableList<Invokable<?, ?>> factoriesToTest = builder.build();
      Assert.assertFalse(
          "No "
              + factoryMethodsDescription

View on GitHub (pinned to 94f39958ba)

Solutions

  1. Read the cause (AssertionFailedError) to see which field/property differs after round-trip.
  2. Ensure equals()/hashCode() depend ONLY on serialized state; remove transient fields from equality, or include their underlying data in serialized form.
  3. If writeReplace changes the representation, implement a symmetric readResolve so equals holds across the boundary.

Example fix

// before
public final class Range implements Serializable {
  private final int lo, hi;
  private transient int cachedHash; // used in hashCode -> diverges after deserialization
  public int hashCode() { return cachedHash; }
}

// after
public final class Range implements Serializable {
  private final int lo, hi;
  public int hashCode() { return lo * 31 + hi; } // computed, not cached transient
}
Defensive patterns

Strategy: validation

Validate before calling

// Round-trip equality self-check.
Object o = factory.invoke(null);
Object r = serializeDeserialize(o);
if (!o.equals(r) || !r.equals(o) || o.hashCode() != r.hashCode())
  throw new AssertionError(factory + " not equal after reserialization");

Prevention

When it happens

Trigger: A factory returns an instance that serializes and deserializes without throwing, but the deserialized instance fails equals() against the original (e.g. hashCode mismatch, equals comparing non-serialized fields, or equals based on mutable/transient state).

Common situations: equals()/hashCode() using a transient or non-serialized field; a class that caches a hash or lazy field and compares it in equals; writeReplace returning a different representative object without a matching readResolve.

Related errors


AI-assisted analysis of google/guava@94f39958ba (2026-08-13). Data as JSON: /api/errors/3695f5673dc59e9c. Report an issue: GitHub.