Kotlin で JSON を使用

# はじめに
Kotlin で JSON を使用した際のことについて記載しています.
最適な方法や間違った表記等があると思いますので, 指摘お願いします.

# 環境
- Kotlin version 1.6.10-release-923 (JRE 17.0.3+3)
- jackson-module-kotlin: 2.13.2
- ~~Kotlin: 1.2.31 (Kotlin version 1.2.31-release-95 (JRE 1.8.0_162-b12))~~
- ~~jackson-module-kotlin: 2.9.5~~

# jackson-module-kotlin の追加
Gradle の使用を想定 ``build.gradle -> dependencies`` に以下を追加

``` groovy:build.gradle
compile group: 'com.fasterxml.jackson.module', name: 'jackson-module-kotlin', version: '2.9.5'
```

::: note info
`LocalDateTime` に対応させるため, 以下のように修正いたしました.
:::
``` groovy:build.gradle
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.13.2'
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.13.2.2'
implementation group: 'com.fasterxml.jackson.module', name: 'jackson-module-kotlin', version: '2.13.2'
implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: '2.13.2'
```

``build.gradle`` については [Kotlin Reference](https://kotlinlang.org/docs/reference/using-gradle.html) を参照してください.


# 単純な JSON
以下の JSON 文字列を対象にします.

```json:paper.json
{
  "serial": 100,
  "id": "pp100",
  "title": "Paper Title",
  "description": "Paper Description"
}
```

::: note info
今回の場合, JSON と クラスのメンバの名称が一致しているため アノテーション ``@JsonPropery`` は付ける必要はありません.
:::

## JSON から Class へ変換
### 1. JSON の形式に沿って以下のクラスを作成します.

```kotlin:Paper.kt
class Paper(
    @JsonProperty("serial")      val serial: Long,
    @JsonProperty("id")          val id: String,
    @JsonProperty("title")       val title: String,
    @JsonProperty("description") val description: String
)
```

### 2. Class への変換

```kotlin:main.kt
import com.fasterxml.jackson.annotation.*
//import com.fasterxml.jackson.databind.*
import com.fasterxml.jackson.module.kotlin.*

class Paper(
    @JsonProperty("serial") val serial: Long,
    @JsonProperty("id") val id: String,
    @JsonProperty("title") val title: String,
    @JsonProperty("description") val description: String
)

fun main(args: Array<String>): Unit {
  // 対象の JSON 文字列
  val jsonString = "{\"serial\":100,\"id\":\"pp100\",\"title\":\"Paper Title\",\"description\":\"Paper Description.\"}"

  /* ObjectMapper を作成
     以下のうち 1 つを使用します. 場合によって import 文を追加してください.
     import com.fasterxml.jackson.databind.*
   */
  //val mapper = ObjectMapper().registerModule(KotlinModule())
  //val mapper = ObjectMapper().registerKotlinModule()
  val mapper = jacksonObjectMapper()

  try {
    //val paper = mapper.readValue<Paper>(jsonString)
    val paper: Paper = mapper.readValue(jsonString)

    println("[${paper.id}] ${paper.title}")
  } catch(e: Exception) {
    e.printStackTrace()
  }
}
```

### 追加 日時(LocalDateTime) を含む Json の変換
この記事を書いた際, Class から Json への変換のみを確認しただけで日付を示す文字列が存在する Json のクラスへの変換は行っていませんでした. 数年ぶりに Android に携わることになり, 今回の問題を確認いたしました.
`LocalDateTime` を含むクラスへ変換を行うと下記の例外発生します(一部省略).
```
Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Java 8 date/time type `java.time.LocalDateTime` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling
 at [Source: (String)"{"id":"f119cc22-33c5-495e-a790-36fd6ff722d7","name":"Example Name","createdAt":"2022-04-08T15:19:10.089517206"}"; line: 1, column: 80] (through reference chain: )
        at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)
        at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1904)
        at com.fasterxml.jackson.databind.deser.impl.UnsupportedTypeDeserializer.deserialize(UnsupportedTypeDeserializer.java:48)
       ...
```

#### `JavaTimeModule` の登録
始めに `jacksonObjectMapper` に対して, `LocalDateTime` の復元処理を追加したモジュールの登録を行います.
```main.kt
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer

val timeModule = JavaTimeModule()
timeModule.addDeserializer<LocalDateTime>(
  LocalDateTime::class.java,
  LocalDateTimeDeserializer(DateTimeFormatter.ISO_DATE_TIME))
mapper.registerModule(timeModule)
```

#### Json から クラスへ変換
下記の Json 文字列を `SampleClass` として定義されたクラスへの変換を行います.
```sample_class.json
{
  "id" : "f119cc22-33c5-495e-a790-36fd6ff722d7",
  "name" : "Example Name",
  "createdAt" : "2022-04-08T15:19:10.089517206"
}
```
```SampleClass.kt
class SampleClass (
  val id: UUID = UUID.randomUUID(),
  val name: String = "Example",
  val createdAt: LocalDateTime = LocalDateTime.now())
```
```main.kt
val sampleJson = "{\"id\":\"f119cc22-33c5-495e-a790-36fd6ff722d7\",\"name\":\"Example Name\",\"createdAt\":\"2022-04-08T15:19:10.089517206\"}";

val sampleClass = mapper.readValue(sampleJson, SampleClass::class.java)
println("${sampleClass.id} ${sampleClass.name} ${sampleClass.createdAt}")
```
実行結果は下記の通りとなり問題なく変換することができます.
```実行結果
f119cc22-33c5-495e-a790-36fd6ff722d7 Example Name 2022-04-08T15:19:10.089517206
```

::: note warn
ここからは個人的に使用していて, 対応したことについて記載しております.
:::
* クラスから Json へ変換した場合に, `LocalDateTime` の形式について.  
Json へ変換した際の `LocalDateTime` の扱いが数値の配列となります.
日付を示す文字列のフォーマットを統一したいため次の調整方法を用いております.
```sample_class.json
{
  "id" : "15dafacb-eaf0-4814-a697-8b6d3cc3628e",
  "name":"Example",
  "createdAt":[2022,4,8,15,32,0,444600164]
}
```
* 調整方法  
`jacksonObjectMapper` に `dateFormat` を設定する.
```main.kt
val mapper = jacksonObjectMapper()
mapper.dateFormat = java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss")
```

## Class から JSON へ変換
上記の記載事項とかぶるところがあるので Class の宣言等は省略します.
文字列への変換は ``ObjectMapper.writeValueAsString(Object value)`` を使用します.

```kotlin:main.kt
import com.fasterxml.jackson.annotation.*
import com.fasterxml.jackson.module.kotlin.*

fun main(args: Array<String>): Unit {
  val paper = Paper(
      serial      = 101,
      id          = "pp101",
      title       = "Paper 101",
      description = "Paper Description"
  )

  try {
    val mapper     = jacksonObjectMapper()
    val jsonString = mapper.writeValueAsString(paper)

    println(jsonString)
  } catch(e: Exception) {
    e.printStackTrace()
  }
}
```

# 継承を利用した Class について
継承した Class を使用する場面があったので, それについて記述します.  

## Class の宣言
Paper クラスを継承するため ``open class Paper`` に変更します.

```kotlin:Paper.kt
open class Paper(
    @JsonProperty("serial") val serial: Long,
    @JsonProperty("id") val id: String,
    @JsonProperty("title") val title: String,
    @JsonProperty("description") val description: String
)

class ExtendPaper(
    serial: Long,
    id: String,
    title: String,
    description: String,
    val registerDateTime: LocalDateTime = LocalDateTime.now()
) : Paper(serial = serial, id = id, title = title, description = description)
```

## 動作確認
以下のコードで動作を確認します.

```kotlin:main.kt
import com.fasterxml.jackson.annotation.*
import com.fasterxml.jackson.module.kotlin.*

open class Paper(
    @JsonProperty("serial") val serial: Long,
    @JsonProperty("id") val id: String,
    @JsonProperty("title") val title: String,
    @JsonProperty("description") val description: String
)

class ExtendPaper(
    serial: Long,
    id: String,
    title: String,
    description: String,
    val registerDateTime: LocalDateTime = LocalDateTime.now()
) : Paper(serial = serial, id = id, title = title, description = description)

fun main(args: Array<String>): Unit {
  val paper = ExtendPaper(
      serial      = 101,
      id          = "pp101",
      title       = "Paper 101",
      description = "Paper Description",
      registerDateTime = LocalDateTime.now()
  )

  try {
    val mapper     = jacksonObjectMapper()
    val jsonString = mapper.writeValueAsString(paper)

    println(jsonString)
  } catch(e: Exception) {
    e.printStackTrace()
  }
}
```

## 出力結果
出力された結果は以下のようになります.

```json:ExtendPaper.json
{
  "serial": 101,
  "id": "pp101",
  "title": "Paper 101",
  "description": "Paper Description",
  "registerDateTime": {
    "year": 2018,
    "monthValue": 4,
    "month": "APRIL",
    "dayOfMonth": 24,
    "dayOfYear": 114,
    "dayOfWeek": "TUESDAY",
    "hour": 12,
    "minute": 29,
    "second": 22,
    "nano": 84000000,
    "chronology": {
      "calendarType": "iso8601",
      "id": "ISO"
    }
  }
}
```

### メンバ変数を JSON に追加させない場合とパースする形を変更
継承した Class で ``LocalDateTime`` がそのまま JSON に変換されてれいる.
このままでも問題はなっかたのですが, ISO 8601 形式で扱いたかったので少し手を加えました.

#### ExtendPaper クラスを変更
``@JsonIgnore``, ``@JsonSetter``, ``@JsonGetter`` の3つを追加します.  


``@JsonIgnore`` を設定することで, そのメンバ変数が変換の対象外になります.
まとめて設定する方法等は下記を参考にしてください.
[Jackson使い方メモ](https://qiita.com/opengl-8080/items/b613b9b3bc5d796c840c)

``@JsonSetter``, ``@JsonGetter`` は ``value`` に対応するメンバ変数を変換する際の専用のメソッドになります.
下記を参考にしてください.
[シリアライズLOBパターンとJAX-RSを組み合わせる(Jackson, Doma)](https://qiita.com/tq_jappy/items/d2cd838e4149f019e391)

```kotlin:ExtendPaper.kt
class ExtendPaper(
    serial: Long,
    id: String,
    title: String,
    description: String,
    @JsonIgnore val registerDateTime: LocalDateTime = LocalDateTime.now()
) : Paper(serial = serial, id = id, title = title, description = description) {

  @JsonGetter(value="registerDateTime")
  private fun ldtToString(): String {
    return this.registerDateTime.toString()
  }

  @JsonSetter(value="registerDateTime")
  private fun ldtFromString(str: String): LocalDateTime {
    return LocalDateTime.parse(str)
  }
}
```

#### 出力結果
変更後の Class から JSON への変換結果は, 以下の様になります.
また, 以下のフォーマットで JSON から Class への変換も可能です.

```json:ExtendPaper.json
{
  "serial": 101,
  "id": "pp101",
  "title": "Paper 101",
  "description": "Paper Description",
  "registerDateTime": "2018-04-24T13:02:59.631"
}
```

# 参照文献
https://github.com/FasterXML/jackson-module-kotlin
[jackson-databind wiki](https://github.com/FasterXML/jackson-databind/wiki)
[Jackson使い方メモ](https://qiita.com/opengl-8080/items/b613b9b3bc5d796c840c)
[シリアライズLOBパターンとJAX-RSを組み合わせる(Jackson, Doma)](https://qiita.com/tq_jappy/items/d2cd838e4149f019e391)