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() when you attempt to register either a class or a label that has already been registered on the same factory instance. Each subtype class and each label string must map one-to-one; registering Circle twice, or registering two classes under the same label, is rejected because it would make the polymorphic mapping ambiguous. This is a configuration-time IllegalArgumentException, not a serialization error.

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 8b8628c656)

Solutions

  1. Audit every registerSubtype call for this factory and ensure each Class and each label String appears exactly once.
  2. If you use the no-arg registerSubtype(type), check Class.getSimpleName() collisions; switch to the two-arg form with explicit unique labels.
  3. Guard registration with a containsKey check on subtypeToLabel/labelToSubtype if registration may run more than once (e.g. from a plugin loader).
  4. Centralize all subtype registration in one place (a single factory builder method) so duplicates are obvious.

Example fix

// before
RuntimeTypeAdapterFactory<Shape> f = RuntimeTypeAdapterFactory.of(Shape.class);
f.registerSubtype(Rectangle.class, "Rect");
f.registerSubtype(Square.class, "Rect"); // throws: duplicate label

// after
f.registerSubtype(Rectangle.class, "Rect");
f.registerSubtype(Square.class, "Square");
Defensive patterns

Strategy: validation

Validate before calling

// Before registering, check both maps for duplicates (reflection or explicit set)
RuntimeTypeAdapterFactory<Shape> f = RuntimeTypeAdapterFactory.of(Shape.class, "type");
List<Class<?>> types = List.of(Circle.class, Rectangle.class, Circle.class);
Set<String> usedLabels = new HashSet<>();
Set<Class<?>> usedTypes = new HashSet<>();
for (Class<?> c : types) {
  String label = c.getSimpleName();
  if (usedTypes.contains(c) || usedLabels.contains(label)) {
    // skip or throw a descriptive error BEFORE calling registerSubtype
    throw new IllegalStateException("Duplicate subtype or label: " + c.getName() + " / " + label);
  }
  usedTypes.add(c); usedLabels.add(label);
  f.registerSubtype(c, label);
}

Type guard

// Type guard: narrow to a registered-subtype registry helper
static boolean isRegistrationUnique(Class<?> type, String label,
    Set<Class<?>> knownTypes, Set<String> knownLabels) {
  return type != null && label != null
      && !knownTypes.contains(type) && !knownLabels.contains(label);
}

Prevention

When it happens

Trigger: Calling factory.registerSubtype(Circle.class, "Circle") followed by factory.registerSubtype(Circle.class) (duplicate class), or factory.registerSubtype(Diamond.class, "Circle") (duplicate label). Also triggered by the no-arg registerSubtype(type) variant when two classes share the same simple name (e.g., two nested classes both named "Builder"), since it defaults the label to type.getSimpleName().

Common situations: Copying subtype registration code across modules and double-registering; using the default simple-name label for inner classes with colliding names; programmatic registration in a loop that re-runs (e.g. scanning a package twice); refactoring a class rename but forgetting to update the label while the old label still gets registered elsewhere.

Related errors


AI-assisted analysis of google/gson@8b8628c656 (2026-08-04). Data as JSON: /data/errors/04a579d7e7ab3dc8.json. Report an issue: GitHub.