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
- Audit every registerSubtype call for this factory and ensure each Class and each label String appears exactly once.
- If you use the no-arg registerSubtype(type), check Class.getSimpleName() collisions; switch to the two-arg form with explicit unique labels.
- Guard registration with a containsKey check on subtypeToLabel/labelToSubtype if registration may run more than once (e.g. from a plugin loader).
- 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
- Centralize all registerSubtype calls in one factory-builder method so duplicates are visible.
- When using the single-arg registerSubtype(type), remember the label defaults to getSimpleName(); check for simple-name collisions across nested classes.
- Build the labelToSubtype map from a single source of truth (e.g. an enum) to avoid drift.
- Add a unit test asserting the factory registers every expected subtype exactly once.
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
- cannot deserialize {baseType} because it does not define a f
- cannot deserialize {baseType} subtype named {label}; did you
- cannot serialize {srcType.getName()}; did you forget to regi
- cannot serialize {srcType.getName()} because it already defi
- Only combinations of \n and \r are allowed in newline.
AI-assisted analysis of google/gson@8b8628c656 (2026-08-04).
Data as JSON: /data/errors/04a579d7e7ab3dc8.json.
Report an issue: GitHub.