Scala
Jackson
Finatra

FinatraのCustom Deserialize

リクエストパラメータの型をjava.time.LocalDateTimeにした時にただの型指定ではエラーが出たので自分でdeserializer書かないといけなかった

リクエストcase classのパラメータは以下のような感じ

import com.twitter.finatra.request.FormParam
import java.time.LocalDateTime

case class CustomRequest(
  @FormParam name: String,
  @FormParam date: Option[LocalDateTime] // ここがデフォルトだとdeserialize出来ない
)

CustomDeserializer書かないままAPIにリクエストすると以下のようなレスポンスが返ってきた
リクエストパラメータはdateが2017-01-20 00:00:00

{
    "errors": {
        "date": "Can not construct instance of java.time.LocalDateTime: no String-argument constructor/factory method to deserialize from String value ('2017-01-20 00:00:00')\n at [Source: N/A; line: -1, column: -1]"
    }
}

参照

今回は先に書いておく
日本語情報無くてつらまってきた
Finatra結構使ってるよという方 :thumbsup: とか絵文字でもいいのでコメント下さい…

Finatra公式 Jackson Integration Adding a Custom Serializer or Deserializer
https://twitter.github.io/finatra/user-guide/json/index.html#customization

finatra/jackson/src/main/scala/com/twitter/finatra/json/internal/serde/FinatraSerDeSimpleModule.scala
https://github.com/twitter/finatra/blob/d0c659e3d139b7c3c1c075133c65a9828a00cbb1/jackson/src/main/scala/com/twitter/finatra/json/internal/serde/FinatraSerDeSimpleModule.scala

finatra/jackson/src/test/scala/com/twitter/finatra/json/tests/internal/ExampleCaseClasses.scala
https://github.com/twitter/finatra/blob/master/jackson/src/test/scala/com/twitter/finatra/json/tests/internal/ExampleCaseClasses.scala

Custom JSON Deserialization with Jackson
https://dzone.com/articles/custom-json-deserialization-with-jackson

Getting Started with Custom Deserialization in Jackson
http://www.baeldung.com/jackson-deserialization

解決手順とコード

解決手順は公式に書いてある、だが例がもうちょっと…
なので動く例も載せる

解決手順

  1. 指定の型のdeserializer書く
  2. そのdeserializerをJacksonModulesに追加作成
  3. FinatraのHttpServerのjacksonModuleを作成したJacksonModulesでoverride

以上

以下はCustomJacksonModuleを作成するコード

CustomJacksonModule.scala
package yourpackage.modules

import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

import com.fasterxml.jackson.annotation.JsonInclude.Include
import com.fasterxml.jackson.core.JsonGenerator.Feature
import com.fasterxml.jackson.core.{JsonParser, ObjectCodec}
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.{DeserializationContext, JsonNode, ObjectMapper, PropertyNamingStrategy}
import com.fasterxml.jackson.databind.module.SimpleModule
import com.twitter.finatra.json.modules.FinatraJacksonModule


// custom deserializer
class CustomLocalDateTimeDeserializer extends JsonDeserializer[LocalDateTime] { // LocalDateTimeが変換したい型
  override def deserialize(jp: JsonParser, ctxt: DeserializationContext) = {
    import scala.util.control.Exception._

    val oc: ObjectCodec = jp.getCodec()
    val node: JsonNode = oc.readTree(jp)
    val strDate: String = node.asText()

    val parseRes = catching(classOf[java.time.format.DateTimeParseException]) opt LocalDateTime.parse(strDate, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
    parseRes match {
      case Some(localDateTime) => localDateTime
      case None => null
    }
  }
}

// Jackson SimpleModule for custom deserializer
class CustomDeserializerModule extends SimpleModule {
  addDeserializer(classOf[LocalDateTime], new CustomLocalDateTimeDeserializer)
}

// custom FinatraJacksonModule which replace the framework module
object CustomJacksonModule extends FinatraJacksonModule {
  override val additionalJacksonModules = Seq(
    new CustomDeserializerModule
  )

  override val serializationInclusion = Include.NON_EMPTY

  override val propertyNamingStrategy = PropertyNamingStrategy.SNAKE_CASE // リクエストパラメータがスネークケース dateOfBirthではなくdate_of_birthじゃないとdeserializeしようとしない

  override def additionalMapperConfiguration(mapper: ObjectMapper) {
    mapper.configure(Feature.WRITE_NUMBERS_AS_STRINGS, true)
  }
}

以下は作成したCustomJacksonModuleでjacksonModuleをoverrideするコード

main.scala
class MainServer extends HttpServer with Logging {
  override def jacksonModule = CustomJacksonModule // ここ書かないと適用されない

  override def configureHttp(router: HttpRouter): Unit = {
    router
      .filter[CommonFilters]
      .add[SomeController]
  }
}

上記を適用すると2017-01-20 00:00:00のようなパラメータをLocalDateTime型で受け取る事ができるようになった
2017-01-20だけだとparseに失敗してdateがOption型なのでNoneになる

以上