※ この投稿は ブログで公開した記事 を一部修正したものです
JAX-RS (Jersey) で Moshi (moshi-kotlin
) 使ってて、雰囲気でレスポンスをreturnさせたら以下のように怒られた:
java.lang.IllegalArgumentException: Expected a Class, ParameterizedType, or GenericArrayType, but <null> is of type null
at com.squareup.moshi.Types.getRawType(Types.java:167)
at com.squareup.moshi.ClassJsonAdapter$1.createFieldBindings(ClassJsonAdapter.java:83)
at com.squareup.moshi.ClassJsonAdapter$1.create(ClassJsonAdapter.java:75)
at com.squareup.moshi.Moshi.adapter(Moshi.java:100)
at com.squareup.moshi.Moshi.adapter(Moshi.java:58)
at com.squareup.moshi.CollectionJsonAdapter.newArrayListAdapter(CollectionJsonAdapter.java:52)
at com.squareup.moshi.CollectionJsonAdapter$1.create(CollectionJsonAdapter.java:36)
at com.squareup.moshi.Moshi.adapter(Moshi.java:100)
at com.squareup.moshi.ClassJsonAdapter$1.createFieldBindings(ClassJsonAdapter.java:91)
at com.squareup.moshi.ClassJsonAdapter$1.create(ClassJsonAdapter.java:75)
at com.squareup.moshi.Moshi.adapter(Moshi.java:100)
at com.squareup.moshi.Moshi.adapter(Moshi.java:58)
at com.jakewharton.moshi.rs.MoshiMessageBodyWriter.writeTo(MoshiMessageBodyWriter.java:57)
at org.glassfish.jersey.message.internal.WriterInterceptorExecutor$TerminalWriterInterceptor.invokeWriteTo(WriterInterceptorExecutor.java:266)
そんなnull
で埋めてる値なんて無いしなぁとか思って放置していたが、さっき気分でMoshiの実装読んでたら納得。勘が悪かった。
要約すると:
- レスポンスに
javax.ws.rs.core.Response
を使ってた -
List<E>
のようなParameterized Typeなオブジェクトを直接entityとして食わせるとダメ
という話。
具体的にはこういうコードのことだ:
// lateinit var ebean: EbeanServer
@GET
@Path("/list")
@Produces(MediaType.APPLICATION_JSON)
fun listItems(): Response {
val items = QMyItm(ebean) // ebean-querybean
.deletedAt.isNull()
.findList()
if(anErrorOccured()) {
return Response.serverError().build()
}
return Response.ok(items).build() // `IllegalArgumentException`
}
// fun anErrorOccured(): Boolean = false
なぜ起きるか
これはJavaの仕様であるType Erasureによるもの。勘の良い人であればResponse
のentity
が Any!
(Object
) になっている時点で気付けたかも。
Type Erasureについては既に語られ尽くしているため詳細な解説を避けるが、今回のケースの場合、要は
-
Response
へentityとしてmyObj: List<MyItm>
が与えられる -
Any!
(Object
) なので、Moshiは型情報を取ろうとする。List.class
であることがわかる - Moshi自身の実装として、
List
の場合はParameterized Typeのindex: 0
を元に展開する必要がある - Parameterized Typeの
index: 0
を取ろうとしたが、これは既にコンパイル時点で消去されている (Type Erasure) - type情報が不明だと続行できないので、
IllegalArgumentException
を投げる
ということ。
とりあえずどうすればいいか
手っ取り早く解決する方法は、
- return typeを
List<E>
のようにする (=Response
使わない) -
MyResponse#getItems
で取れるような形でwrapする (= Parameterized Typeでないオブジェクトにする)
など。
恐らくResponse
を使っている理由はstatus codeなんかを適宜変えたいからだと思うのだけど、ExceptionMapper
を実装してあげて自前のThrowableを投げればレスポンスを変えられるので、前者を取ることをお勧めする。
たとえば以下のような形になる:
// lateinit var ebean: EbeanServer
@GET
@Path("/list")
@Produces(MediaType.APPLICATION_JSON)
fun listItems(): List<MyItm>? {
val items = QMyItm(ebean) // ebean-querybean
.deletedAt.isNull()
.findList()
if(anErrorOccured()) {
throw MySomethingWrongException()
}
return items
}
// fun anErrorOccured(): Boolean = false
// class MySomethingWrongException : RuntimeException()
// もしResponse<T>
のような仕様だったら、こういった事は起こらなかったのかも