0
0

RetorofitでGenericsを使おうとしてParameter type must not include a type variable or wildcardが発生した時の記録

Last updated at Posted at 2024-07-10

やりたかったこと

サーバーにログを送信するAPI。
ログの種類によってPayloadが変わるRetorofitのIFをジェネリクスにして楽したい。

interface SampleApi {
    @POST("log/")
    suspend fun <T : Payload> postLog(
        @Body
        log: LogData<T>
    ): Response<Unit>
}

@JsonClass(generateAdapter = true)
data class LogData<T : Payload>(
    @Json(name = "event_type")
    val eventType: String,
    @Json(name = "payload")
    val payload: T
)

interface Payload {
    val id: String
}

@JsonClass(generateAdapter = true)
data class NotificationPayload(
    @Json(name = "message")
    val message: String,
    @Json(name = "id")
    override val id: String
): Payload


@JsonClass(generateAdapter = true)
data class AppLunchPayload(
    @Json(name = "application_id")
    val applicationId: Double,
    @Json(name = "id")
    override val id: String
): Payload

問題

しかしこのAPIを呼び出すとなんと実行時に例外が発生してしまう。
どうやらBodyのデータでGenericsを指定している部分が良くないらしい...

java.lang.IllegalArgumentException: Parameter type must not include a type variable or wildcard: com.example.retorofit.model.LogData<T> (parameter #1)
    for method SampleApi.postLog
	at retrofit2.Utils.methodError(Utils.java:56)
	at retrofit2.Utils.methodError(Utils.java:44)
	at retrofit2.Utils.parameterError(Utils.java:68)
	at retrofit2.RequestFactory$Builder.validateResolvableType(RequestFactory.java:827)
	at retrofit2.RequestFactory$Builder.parseParameterAnnotation(RequestFactory.java:781)
	at retrofit2.RequestFactory$Builder.parseParameter(RequestFactory.java:335)
	at retrofit2.RequestFactory$Builder.build(RequestFactory.java:213)
	at retrofit2.RequestFactory.parseAnnotations(RequestFactory.java:67)
	at retrofit2.ServiceMethod.parseAnnotations(ServiceMethod.java:26)
	at retrofit2.Retrofit.loadServiceMethod(Retrofit.java:235)
	at retrofit2.Retrofit$1.invoke(Retrofit.java:177)
	at jdk.proxy3/jdk.proxy3.$Proxy16.postLog(Unknown Source)
	at 
 ...

解決策

RetrofitのIFでジェネリック型ではなく実装クラスを指定する。

public interface SampleApi {
    
    @POST("log/")
    suspend fun postNotificationLog(
        @Body
        log: LogData<NotificationPayload>
    ): Response<Unit>

    @POST("log/")
    suspend fun postAppLunchLog(
        @Body
        log: LogData<AppLunchPayload>
    ): Response<Unit>

    /*
    @POST("log/")
    suspend fun <T : Payload> postLog(
        @Body
        log: LogData<T>
    ): Response<Unit>
    */
}

無事例外が発生しなくなった。めでたしめでたし

おまけ: 解決するまでにやったこと

JvmSuppressWildcardsを指定する

ネット上にいくつか先人がおり @JvmSuppressWildcards を使うと良いと書いてある。
kotlinからjavaコードに変換される際、ジェネリクスがwildcardに変換されてしまうことがあるようだ。

@JvmSuppressWildcards を使うようにコードを書き直してみたが呼び出し時の例外は解消されない。困った。

interface SampleApi {
    @POST("log/")
    @JvmSuppressWildcards
    suspend fun <T : Payload> postLog(
        @Body
        log: LogData<T>
    ): Response<Unit>
}

@JsonClass(generateAdapter = true)
@JvmSuppressWildcards
data class LogData<T : Payload>(
    @Json(name = "event_type")
    val eventType: String,
    @Json(name = "payload")
    val payload: T
)

interface Payload {
    val id: String
}

@JsonClass(generateAdapter = true)
data class NotificationPayload(
    @Json(name = "message")
    val message: String,
    @Json(name = "id")
    override val id: String
): Payload


@JsonClass(generateAdapter = true)
data class AppLunchPayload(
    @Json(name = "application_id")
    val applicationId: Double,
    @Json(name = "id")
    override val id: String
): Payload

Javaコードを確認

AndroidStudioでkotlinからjavaコードが生成できるのでどのようなコードになっているか見てみることにした。
wildcardにはなっていない :thinking:
ということはtype variableということになる


public interface SampleApi {
   @POST("log/")
   @JvmSuppressWildcards
   @Nullable
   Object postLog(@Body @NotNull LogData var1, @NotNull Continuation var2);
}

@JvmSuppressWildcards
public final class LogData {
   @NotNull
   private final String eventType;
   @NotNull
   private final Payload payload;

   @NotNull
   public final String getEventType() {
      return this.eventType;
   }

   @NotNull
   public final Payload getPayload() {
      return this.payload;
   }

   public LogData(
       @Json(name = "event_type") 
       @NotNull 
       String eventType, 
       
       @Json(name = "payload") 
       @NotNull 
       Payload payload
    ) {
      ...
   }

   @NotNull
   public final String component1() {
      return this.eventType;
   }

   @NotNull
   public final Payload component2() {
      return this.payload;
   }

   ...
}

Retrofitのソースコードを見てみる

デバッグをしながらRetrofitのソースコードを追ってみるとtype=TがTypeVariableと判定されエラーになっていた。

package retrofit2;
final class RequestFactory {
    private void validateResolvableType(int p, Type type) {
        if (Utils.hasUnresolvableType(type)) {
            throw Utils.parameterError(this.method, p, "Parameter type must not include a type variable or wildcard: %s", new Object[]{type});
        }
    }
}
package retrofit2;
final class Utils {
    static boolean hasUnresolvableType(@Nullable Type type) {
        if (type instanceof Class) {
            return false;
        } else if (type instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) type;
            Type[] var2 = parameterizedType.getActualTypeArguments();
            int var3 = var2.length;

            for (int var4 = 0; var4 < var3; ++var4) {
                Type typeArgument = var2[var4];
                if (hasUnresolvableType(typeArgument)) {
                    return true;
                }
            }

            return false;
        } else if (type instanceof GenericArrayType) {
            return hasUnresolvableType(((GenericArrayType) type).getGenericComponentType());
        } else if (type instanceof TypeVariable) {
            return true;
        } else if (type instanceof WildcardType) {
            return true;
        } else {
            String className = type == null ? "null" : type.getClass().getName();
            throw new IllegalArgumentException("Expected a Class, ParameterizedType, or GenericArrayType, but <" + type + "> is of type " + className);
        }
    }
}

TypeVariableとは???

TypeVariableは、型変数の種類の共通のスーパー・インタフェースです。型変数は、このパッケージで指定されているように、リフレクト・メソッドにより必要とされるときにはじめて作成されます。型変数tが型(つまり、クラス、インタフェース、あるいは注釈型) Tにより参照される場合、TはTを囲むn番目のクラスにより宣言されます(JLS 8.1.2を参照)。

とても難解だ...

とりあえずRetrofitはジェネリクス型はパラメーターとして利用できないということはわかった。<T>を使わない方法でAPIのIFを書くことにした。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0