LoginSignup
0
1

More than 5 years have passed since last update.

JAX-RSでMoshi使ったら `~but <null> is of type null` と怒られてハマった

Posted at

※ この投稿は ブログで公開した記事 を一部修正したものです


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によるもの。勘の良い人であればResponseentityAny! (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>のような仕様だったら、こういった事は起こらなかったのかも

0
1
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
1