LoginSignup
30
27

More than 1 year has passed since last update.

Kotlin で JSON を使用

Last updated at Posted at 2018-04-24

はじめに

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 に以下を追加

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

LocalDateTime に対応させるため, 以下のように修正いたしました.

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 を参照してください.

単純な JSON

以下の JSON 文字列を対象にします.

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

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

JSON から Class へ変換

1. JSON の形式に沿って以下のクラスを作成します.

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 への変換

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

ここからは個人的に使用していて, 対応したことについて記載しております.

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

Class から JSON へ変換

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

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 に変更します.

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)

動作確認

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

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()
  }
}

出力結果

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

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使い方メモ

@JsonSetter, @JsonGettervalue に対応するメンバ変数を変換する際の専用のメソッドになります.
下記を参考にしてください.
シリアライズLOBパターンとJAX-RSを組み合わせる(Jackson, Doma)

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 への変換も可能です.

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
Jackson使い方メモ
シリアライズLOBパターンとJAX-RSを組み合わせる(Jackson, Doma)

30
27
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
30
27