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

  1. Make the referenced class extend TypeAdapter<T> (or implement one of the other three interfaces).
  2. Correct the annotation to reference the actual adapter class.
  3. If the class must stay as-is, wrap it in a TypeAdapter implementation.
  4. 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

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


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)