google/gson · error · RuntimeException

Unexpected ReflectiveOperationException occurred

Error message

Unexpected ReflectiveOperationException occurred (Gson {}). To support Java records, reflection is utilized to read out information about records. All these invocations happens after it is established that records exist in the JVM. This exception is unexpected behavior.

What it means

Thrown by ReflectionHelper when record-related reflection (Class.isRecord, RecordComponent access, canonical constructor lookup, accessor method invocation) fails with ReflectiveOperationException after the JVM was detected to support records. The code path only runs on Java 14+ (records preview) or 16+ (stable); failure here is unexpected and indicates a JVM or toolchain anomaly.

Solutions

  1. Run on a stable Java LTS (17 or 21) where records are fully supported and stable.
  2. Ensure no bytecode tooling (old Lombok, old ASM/ByteBuddy, obfuscators) strips the Record attribute or RecordComponent metadata from the compiled class.
  3. If the type is not truly a record, refactor it to a plain class so Gson does not take the record reflection path.
  4. Register a custom TypeAdapter<YourRecord> that constructs the record via its canonical constructor, bypassing record reflection.
  5. Report the failure to Gson maintainers with the JVM vendor/version and the record class, as the message indicates this is unexpected.

Example fix

// before: record deserialized via reflection on a flaky JVM
public record Point(int x, int y) {}
Point p = gson.fromJson("{\"x\":1,\"y\":2}", Point.class); // throws

// after: custom adapter using canonical constructor directly
Gson g = new GsonBuilder()
  .registerTypeAdapter(Point.class, new TypeAdapter<Point>() {
    public Point read(JsonReader in) throws IOException {
      in.beginObject(); int x=0,y=0;
      while (in.hasNext()) {
        switch (in.nextName()) {
          case "x": x = in.nextInt(); break;
          case "y": y = in.nextInt(); break;
          default: in.skipValue();
        }
      }
      in.endObject();
      return new Point(x, y);
    }
    public void write(JsonWriter out, Point p) throws IOException {
      out.beginObject().name("x").value(p.x()).name("y").value(p.y()).endObject();
    }
  }).create();
Defensive patterns

Strategy: fallback

Validate before calling

// detect record metadata stability at startup
try {
  Class.forName("java.lang.reflect.RecordComponent");
  // smoke test a real record round-trip
  Object r = gson.fromJson("{\"x\":1,\"y\":2}", com.example.Point.class);
} catch (Throwable t) {
  throw new IllegalStateException("Record reflection unstable on this JVM/toolchain", t);
}

Type guard

public static boolean isStableRecord(Class<?> c) {
  if (!c.isRecord()) return false;
  try { c.getRecordComponents(); c.getDeclaredConstructor(
      java.util.Arrays.stream(c.getRecordComponents())
        .map(rc -> rc.getType()).toArray(Class<?>[]::new)); return true; }
  catch (Throwable t) { return false; }
}

Try / catch

try {
  return gson.fromJson(json, Point.class);
} catch (RuntimeException e) {
  if (e.getMessage() != null && e.getMessage().contains("Unexpected ReflectiveOperationException occurred")) {
    // fall back to a custom TypeAdapter<YourRecord> using the canonical constructor
    return customPointGson().fromJson(json, Point.class);
  }
  throw e;
}

Prevention

When it happens

Trigger: Deserializing or serializing a Java record on a JVM that reported records as supported, but a subsequent reflective call (getting record components, canonical constructor, or accessor method) throws. Seen with non-standard JVMs, bytecode manipulation tools (some older Lombok/MapStruct versions), partial record support, or future/preview record features.

Common situations: Preview record features on early JDK builds; class file transformations that strip Record attributes; custom classloaders hiding record metadata; Kotlin/Scala data classes masquerading as records; running on a JVM whose java.lang.reflect.RecordComponents API is inconsistent with the actual bytecode.

Related errors


AI-assisted analysis of google/gson@310ac341f2 (2026-08-10). Data as JSON: /api/errors/6b7b1cf21946194a. Report an issue: GitHub.

Appendix: source

Thrown at gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java:211

  }

  public static <T> Constructor<T> getCanonicalRecordConstructor(Class<T> raw) {
    return RECORD_HELPER.getCanonicalRecordConstructor(raw);
  }

  public static RuntimeException createExceptionForUnexpectedIllegalAccess(
      IllegalAccessException exception) {
    throw new RuntimeException(
        "Unexpected IllegalAccessException occurred (Gson "
            + GsonBuildConfig.VERSION
            + "). Certain ReflectionAccessFilter features require Java >= 9 to work correctly. If"
            + " you are not using ReflectionAccessFilter, report this to the Gson maintainers.",
        exception);
  }

  private static RuntimeException createExceptionForRecordReflectionException(
      ReflectiveOperationException exception) {
    throw new RuntimeException(
        "Unexpected ReflectiveOperationException occurred"
            + " (Gson "
            + GsonBuildConfig.VERSION
            + ")."
            + " To support Java records, reflection is utilized to read out information"
            + " about records. All these invocations happens after it is established"
            + " that records exist in the JVM. This exception is unexpected behavior.",
        exception);
  }

  /** Internal abstraction over reflection when Records are supported. */
  private abstract static class RecordHelper {
    abstract boolean isRecord(Class<?> clazz);

    abstract String[] getRecordComponentNames(Class<?> clazz);

    abstract <T> Constructor<T> getCanonicalRecordConstructor(Class<T> raw);

View on GitHub (pinned to 310ac341f2)