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で埋めてる値なんて無いしなぁとか思いながら昨日の深夜3時頃は 諦めてラーメン食べ行った のだけど、さっき気分で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であることがわかるListの場合はParameterized Typeのindex: 0を元に展開する必要があるindex: 0を取ろうとしたが、これは既にコンパイル時点で消去されている (Type Erasure)IllegalArgumentExceptionを投げるということ。
手っ取り早く解決する方法は、
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>のような仕様だったら、こういった事は起こらなかったのかも