FinatraのCustom Deserialize



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

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

```scala
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`

```json
{
	"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結構使ってるよという方 :+1: とか絵文字でもいいのでコメント下さい…

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を作成するコード

```scala: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するコード

```scala: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になる


以上