google/gson · error · IllegalArgumentException

types and labels must be unique

Error message

types and labels must be unique

What it means

Thrown by RuntimeTypeAdapterFactory.registerSubtype(Class, String) when the subtype class or the label string has already been registered on the same factory. The factory keeps two maps (subtypeToLabel and labelToSubtype) and rejects any duplicate on either side to keep the type<->label mapping strictly one-to-one. This is a configuration-time IllegalArgumentException, fired before any serialization occurs.

Solutions

  1. Search your registration calls for the duplicated class or label name reported in the failure.
  2. If two classes share a simple name, pass an explicit unique label as the second argument to registerSubtype.
  3. Make sure the RuntimeTypeAdapterFactory is constructed fresh per GsonBuilder.create() and not reused/shared across builds.
  4. Build the label set programmatically and assert uniqueness before registering (e.g. collect labels into a Set).

Example fix

// before
factory.registerSubtype(Circle.class);
factory.registerSubtype(Circle.class); // duplicate class -> throws

// after
factory.registerSubtype(Circle.class, "circle");
factory.registerSubtype(Square.class, "square");
Defensive patterns

Strategy: validation

Validate before calling

// Before building the factory, assert no duplicate class or label
Set<Class<?>> classes = new HashSet<>();
Set<String> labels = new HashSet<>();
for (var reg : registrations) {
  if (!classes.add(reg.type) || !labels.add(reg.label)) {
    throw new IllegalStateException("Duplicate subtype registration: " + reg);
  }
}
// then safely call factory.registerSubtype(reg.type, reg.label)

Prevention

When it happens

Trigger: Calling registerSubtype(SameClass, ...) twice; calling registerSubtype(ClassA, "foo") and registerSubtype(ClassB, "foo") (duplicate label); calling the single-arg registerSubtype(Class) for two classes whose getSimpleName() collide; re-registering after reusing one factory instance across builds.

Common situations: Two inner classes share a simple name (e.g. two Foo$Bar); a label typo reuses an existing discriminator; building the factory inside a loop or helper that runs more than once; copy-pasting registration blocks; mixing label-based and simple-name registration.

Related errors


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

Appendix: source

Thrown at extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java:224

  @CanIgnoreReturnValue
  public RuntimeTypeAdapterFactory<T> recognizeSubtypes() {
    this.recognizeSubtypes = true;
    return this;
  }

  /**
   * Registers {@code type} identified by {@code label}. Labels are case sensitive.
   *
   * @throws IllegalArgumentException if either {@code type} or {@code label} have already been
   *     registered on this type adapter.
   */
  @CanIgnoreReturnValue
  public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type, String label) {
    if (type == null || label == null) {
      throw new NullPointerException();
    }
    if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) {
      throw new IllegalArgumentException("types and labels must be unique");
    }
    labelToSubtype.put(label, type);
    subtypeToLabel.put(type, label);
    return this;
  }

  /**
   * Registers {@code type} identified by its {@link Class#getSimpleName simple name}. Labels are
   * case sensitive.
   *
   * @throws IllegalArgumentException if either {@code type} or its simple name have already been
   *     registered on this type adapter.
   */
  @CanIgnoreReturnValue
  public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type) {
    return registerSubtype(type, type.getSimpleName());
  }

View on GitHub (pinned to 310ac341f2)