google/gson · error · IllegalArgumentException
Invalid attempt to bind an instance of
Error message
Invalid attempt to bind an instance of ${className} as a @JsonAdapter for ${type}. @JsonAdapter value must be a TypeAdapter, TypeAdapterFactory, JsonSerializer or JsonDeserializer. What it means
JsonAdapterAnnotationTypeAdapterFactory constructs an instance of the class referenced by @JsonAdapter and checks it is one of TypeAdapter, TypeAdapterFactory, JsonSerializer, or JsonDeserializer. If the class implements none of these, it throws IllegalArgumentException at adapter-resolution time (when Gson first requests an adapter for the annotated type).
Solutions
- Make the referenced class extend TypeAdapter<T> (or implement one of the other three interfaces).
- Correct the annotation to reference the actual adapter class.
- If the class must stay as-is, wrap it in a TypeAdapter implementation.
- Add a unit test that does gson.toJson / fromJson on the annotated type to catch the failure early.
Example fix
// before
@JsonAdapter(PlainConfig::class)
data class User(val name: String)
class PlainConfig // implements nothing
// after
class PlainConfig : TypeAdapter<User>() {
override fun write(out: JsonWriter, v: User) { out.value(v.name) }
override fun read(`in`: JsonReader): User = User(`in`.nextString())
} Defensive patterns
Strategy: validation
Validate before calling
// Verify at startup that every @JsonAdapter value is a supported adapter type
static void validateJsonAdapters(Class<?>... types) {
for (Class<?> c : types) {
JsonAdapter a = c.getAnnotation(JsonAdapter.class);
if (a == null) continue;
Class<?> v = a.value();
boolean ok = TypeAdapter.class.isAssignableFrom(v)
|| TypeAdapterFactory.class.isAssignableFrom(v)
|| JsonSerializer.class.isAssignableFrom(v)
|| JsonDeserializer.class.isAssignableFrom(v);
if (!ok) throw new IllegalStateException(c + " @JsonAdapter value " + v + " is not an adapter");
}
} Type guard
static boolean isValidAdapterClass(Class<?> c) {
return TypeAdapter.class.isAssignableFrom(c)
|| TypeAdapterFactory.class.isAssignableFrom(c)
|| JsonSerializer.class.isAssignableFrom(c)
|| JsonDeserializer.class.isAssignableFrom(c);
} Try / catch
try {
gson.fromJson(json, Annotated.class);
} catch (IllegalArgumentException e) {
if (e.getMessage().contains("@JsonAdapter value must be")) {
// fix the referenced class to implement one of the four interfaces
log.error("Bad @JsonAdapter on {}: {}", Annotated.class, e.getMessage());
}
throw e;
} Prevention
- Always extend TypeAdapter or implement TypeAdapterFactory for @JsonAdapter classes.
- Add a startup self-test that constructs a Gson and round-trips every annotated type.
- Review @JsonAdapter targets during code review.
- Keep adapter classes final and dedicated (single responsibility).
When it happens
Trigger: Annotating a type or field with @JsonAdapter(MyClass.class) where MyClass does not extend TypeAdapter nor implement TypeAdapterFactory/JsonSerializer/JsonDeserializer. Fires the first time Gson serializes/deserializes the annotated type.
Common situations: Forgetting to extend TypeAdapter; pointing the annotation at a plain POJO or a data class; refactoring a class to no longer implement the expected interface without updating the annotation.
Related errors
- Class does not implement any supported type adapter class…
- GSON ( ) cannot handle
- @SerializedName on is not supported
- Type adapter must implement JsonSerializer or…
- Adapter for type with cyclic dependency has been used…
AI-assisted analysis of google/gson@310ac341f2 (2026-08-10).
Data as JSON: /api/errors/21d1ea3ef361e4ec.
Report an issue: GitHub.
Appendix: source
Thrown at gson/src/main/java/com/google/gson/internal/bind/JsonAdapterAnnotationTypeAdapterFactory.java:148
// Uses dummy factory instances because TreeTypeAdapter needs a 'skipPast' factory for
// `Gson.getDelegateAdapter` call and has to differentiate there whether TreeTypeAdapter was
// created for @JsonAdapter on class or field
TypeAdapterFactory skipPast;
if (isClassAnnotation) {
skipPast = TREE_TYPE_CLASS_DUMMY_FACTORY;
} else {
skipPast = TREE_TYPE_FIELD_DUMMY_FACTORY;
}
@SuppressWarnings({"unchecked", "rawtypes"})
TypeAdapter<?> tempAdapter =
new TreeTypeAdapter(serializer, deserializer, gson, type, skipPast, nullSafe);
typeAdapter = tempAdapter;
// TreeTypeAdapter handles nullSafe; don't additionally call `nullSafe()`
nullSafe = false;
} else {
throw new IllegalArgumentException(
"Invalid attempt to bind an instance of "
+ instance.getClass().getName()
+ " as a @JsonAdapter for "
+ type.toString()
+ ". @JsonAdapter value must be a TypeAdapter, TypeAdapterFactory,"
+ " JsonSerializer or JsonDeserializer.");
}
if (typeAdapter != null && nullSafe) {
typeAdapter = typeAdapter.nullSafe();
}
return typeAdapter;
}
@SuppressWarnings("ReferenceEquality")
private static boolean areSameFactories(TypeAdapterFactory a, TypeAdapterFactory b) {
// Checks for reference equality, like it is done by `Gson.getDelegateAdapter`View on GitHub (pinned to 310ac341f2)