やりたかったこと
サーバーにログを送信する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にはなっていない
ということは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を書くことにした。