google/gson · error · IllegalArgumentException

Invalid attempt to bind an instance of {className} as a @Jso

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 throws IllegalArgumentException when the class referenced by @JsonAdapter is instantiated but is none of TypeAdapter, TypeAdapterFactory, JsonSerializer, or JsonDeserializer. Gson supports only those four binding types as the value of the annotation; anything else (or a class that implements none of them) is a programming error detected at adapter-binding time. The {className} is the adapter class, {type} is the type being bound.

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

Solutions

  1. Make the referenced class implement exactly one of TypeAdapter, TypeAdapterFactory, JsonSerializer, or JsonDeserializer (and implement the required method).
  2. If you meant to reference a factory that builds adapters, implement TypeAdapterFactory and override create(Gson, TypeToken).
  3. Remove the @JsonAdapter annotation if no custom adapter is needed.
  4. Rebuild/redeploy — this is detected at first adapter lookup so a stale class file can also cause it.

Example fix

// before
@JsonAdapter(MyDto.class) // MyDto is a plain DTO, not an adapter
class MyDto { String s; }

// after
@JsonAdapter(MyDtoAdapter.class)
class MyDto { String s; }
class MyDtoAdapter extends TypeAdapter<MyDto> {
  @Override public MyDto read(JsonReader in) throws IOException { /* ... */ }
  @Override public void write(JsonWriter out, MyDto v) throws IOException { /* ... */ }
}
Defensive patterns

Strategy: type-guard

Validate before calling

// Verify the @JsonAdapter value implements a supported interface at registration time
Class<?> c = annotation.value();
if (!TypeAdapter.class.isAssignableFrom(c)
    && !TypeAdapterFactory.class.isAssignableFrom(c)
    && !JsonSerializer.class.isAssignableFrom(c)
    && !JsonDeserializer.class.isAssignableFrom(c)) {
  throw new IllegalArgumentException(c.getName() + " is not a valid @JsonAdapter value");
}

Type guard

static boolean isValidJsonAdapterValue(Class<?> c) {
  return TypeAdapter.class.isAssignableFrom(c)
      || TypeAdapterFactory.class.isAssignableFrom(c)
      || JsonSerializer.class.isAssignableFrom(c)
      || JsonDeserializer.class.isAssignableFrom(c);
}

Try / catch

try {
  return gson.getAdapter(MyType.class);
} catch (IllegalArgumentException e) {
  if (e.getMessage() != null && e.getMessage().contains("@JsonAdapter value must be")) {
    throw new ConfigurationException("Invalid @JsonAdapter on " + MyType.class, e);
  }
  throw e;
}

Prevention

When it happens

Trigger: Annotating a type or field with @JsonAdapter(MyX.class) where MyX does not implement any of the four supported interfaces. Often MyX is a helper/DTO, an old-style adapter, or a class intended for a different JSON library.

Common situations: Copy/paste from a tutorial for a different library; renaming a class so it no longer implements TypeAdapter; annotating with an interface instead of the concrete adapter; leftover annotation after refactor.

Related errors


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