kotlinx.serialization をマルチプラットフォームで使う

# はじめに

昨年は [Kotlin/JS の現状を確かめる](https://qiita.com/nosix/items/45a8054a93455a69afab) と題して、Kotlin/JS まわりを試してみました。

今回の題材はこちら。

[Kotlin cross-platform / multi-format reflectionless serialization](https://github.com/Kotlin/kotlinx.serialization)

マルチプラットフォーム(JVM, JS, Native)で使えるシリアライゼーションの仕組みです。
コンパイラプラグインとランタイムライブラリで構成されています。
JSON, CBOR, Protobuf 形式に変換できる様です。
色々と試したいところですが、JSON 変換を試してみたいと思います。

JSON 変換ライブラリと言えば、Android では [GSON](https://github.com/google/gson), [Moshi](https://github.com/square/moshi)、Spring では [Jackson](https://github.com/FasterXML/jackson) などと様々存在するわけですが、バックエンドとフロントエンドで異なるライブラリを使うとそれぞれに学習する必要があり、また各々カスタマイズが必要だったりするかと思います。

共通化するならば、Kotlin 標準はどうだろう？ということで今回の題材です。目指すは、バックエンドとフロントエンドでの JSON 変換の共通化。以下の図の構成を目指した一歩になります。

<img src="https://docs.google.com/drawings/d/e/2PACX-1vSDi0jO2Nzf4RqHnX3yPSminOAXefnF_Ufuu1RhnYY1mHA8ohQTTXW9KsP2RplGza7PMyIZKx0qRto9/pub?w=960&amp;h=720">

今回の記事のコードを読みたい方は [kotlin-serialization-sample](https://github.com/nosix/kotlin-serialization-sample) をどうぞ。

# マルチプラットフォームライブラリを構成する

マルチプラットフォームで試すために、まずは環境を構築します。

[Building Multiplatform Projects with Gradle](https://kotlinlang.org/docs/reference/building-mpp-with-gradle.html)

目指すは、Android, iOS, Web での共通化なのですが最初からそこを目指すと遭難しそうなので、まずは Kotlin/JVM, Kotlin/Native, Kotlin/JS を全て使うところから始めます。

## ディレクトリ構成

```
/
|- gradlew
|- gradlew.bat
|- gradle
|  |- wrapper
|     |- gradle-wrapper.jar
|     |- gradle-wrapper.properties
|- gradle.properties
|- settings.gradle
|- sample-library
   |- build.gradle
   |- src
      |- commonMain
      |- commonTest
      |- jsMain
      |- jsTest
      |- jvmMain
      |- jvmTest
      |- macosMain
      |- macosTest
```

## Gradle バージョン

今回のサンプルでは [Gradle Wrapper](http://gradle.monochromeroad.com/docs/userguide/gradle_wrapper.html) を使っています。Gradle のバージョンは gradle-wrapper.properties で指定しています。

```properties:gradle-wrapper.properties
distributionUrl=https\://services.gradle.org/distributions/gradle-4.7-all.zip
```

今回は Native の環境を含むため、バージョン **4.7** を使う必要があります。

> You must have Gradle 4.7, because higher versions have unsupported format of metadata. 
>
> 引用：https://github.com/Kotlin/kotlinx.serialization#native

## gradle.properties

Gradle plugin や依存ライブラリのバージョンの指定には gradle.properties を使うことにします。

```properties:gradle.properties
kotlinVersion=1.3.11
kotlinSerializationVersion=0.9.1
```

後の話になりますが、Gradle plugin の version 指定に変数が使えない問題に対処するためです。詳しくは、[Allow the plugin DSL to expand properties as part of the version](https://github.com/gradle/gradle/issues/1697#issuecomment-386824663) を参照。

## settings.gradle

```groovy:settings.gradle
pluginManagement {
    resolutionStrategy {
        eachPlugin {
            if (requested.id.id == "kotlin-multiplatform") {
                useModule("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
            }
            if (requested.id.id == "kotlinx-serialization") {
                useModule("org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion")
            }
        }
    }
}

include ':sample-library'

rootProject.name = 'kotlin-serialization-sample'

// for native
enableFeaturePreview('GRADLE_METADATA')
```

multiplatform Gradle plugin と serialization Gradle plugin は Gradle plugin portal に公開されていないため、pluginManagement に設定を追加します。[Gradle (with plugins block)](https://github.com/Kotlin/kotlinx.serialization#gradle-with-plugins-block) に書かれています。バージョン指定の方法を変更していますが、gradle.properties で指定したバージョンを使用するための処置です。

また、Native で使う場合には、`enableFeaturePreview('GRADLE_METADATA')` が必要です。[シリアライゼーションの Native の説明](https://github.com/Kotlin/kotlinx.serialization#native) に書かれています。

## build.gradle

sample-library の build.gradle です。今回のサンプルの肝になる部分です。

```groovy:build.gradle
buildscript {
    repositories {
        mavenCentral()
    }
}

plugins {
    id 'kotlin-multiplatform'
    id 'kotlinx-serialization'
}
repositories {
    mavenCentral()
    // for kotlinx-serialization-runtime
    maven { url "https://kotlin.bintray.com/kotlinx" }
}
group 'com.example'
version '0.0.1'

apply plugin: 'maven-publish'

kotlin {
    targets {
        fromPreset(presets.jvm, 'jvm')
        fromPreset(presets.js, 'js')
        // For ARM, preset should be changed to presets.iosArm32 or presets.iosArm64
        // For Linux, preset should be changed to e.g. presets.linuxX64
        // For MacOS, preset should be changed to e.g. presets.macosX64
        fromPreset(presets.macosX64, 'macos')
    }
    sourceSets {
        commonMain {
            dependencies {
                implementation 'org.jetbrains.kotlin:kotlin-stdlib-common'
                implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:$kotlinSerializationVersion"
            }
        }
        commonTest {
            dependencies {
                implementation 'org.jetbrains.kotlin:kotlin-test-common'
                implementation 'org.jetbrains.kotlin:kotlin-test-annotations-common'
            }
        }
        jvmMain {
            dependencies {
                implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
                implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:$kotlinSerializationVersion"
            }
        }
        jvmTest {
            dependencies {
                implementation 'org.jetbrains.kotlin:kotlin-test'
                implementation 'org.jetbrains.kotlin:kotlin-test-junit'
            }
        }
        jsMain {
            dependencies {
                implementation 'org.jetbrains.kotlin:kotlin-stdlib-js'
                implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-js:$kotlinSerializationVersion"
            }
        }
        jsTest {
            dependencies {
                implementation 'org.jetbrains.kotlin:kotlin-test-js'
            }
        }
        macosMain {
            dependencies {
                implementation 'org.jetbrains.kotlin:kotlin-stdlib'
                implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-native:$kotlinSerializationVersion"
            }
        }
        macosTest {
        }
    }
}
```

まず、plugins で multiplatform plugin と serialization plugin を指定しています。settings.gradle で設定した plugin を指定しています。ここで version を指定できるのですが、version の指定に変数を使うことができないため settings.gradle で version を指定しています。

targets では、presets から target platform を選択しています。presets の一覧は [Supported platforms](https://kotlinlang.org/docs/reference/building-mpp-with-gradle.html#supported-platforms) で確認できます。`presets.jvm` に続く `'jvm'` は targetName です。任意の名前を付けられます。targetName は sourceSets におけるプレフィックスになり、ディレクトリ名と sourceSets の指定で使われます。例えば、targetName が `'jvm'` ならば `jvmMain`, `jvmTest` を src ディレクトリに加えます。そして、sourceSets に `jvmMain`, `jvmTest` に対する設定を書きます。

各 sourceSets では serialization runtime ライブラリを依存に追加します。JVM, JS, Native で異なるので注意が必要です。

# Serialization サンプルコード

## common コードでテスト

[Quick example](https://github.com/Kotlin/kotlinx.serialization#quick-example) をアレンジして試してみます。

```
src
|- commonMain
|  |- resources
|  |- kotlin
|     |- sample
|        |- Data.kt
|- commonTest
   |- resources
   |- kotlin
      |- sample
         |- SerializationTests.kt
```

resources ディレクトリは不要ですが、ディレクトリ構成のイメージを補うために書いています。Main の kotlin ディレクトリに `sample.Data` クラスのコード、Test の kotlin ディレクトリに `sample.SerializationTests` クラスのコードを配置しています。

```kotlin:Data.kt
package sample

import kotlinx.serialization.Optional
import kotlinx.serialization.Serializable

@Serializable
data class Data(val a: Int, @Optional val b: String = "42")
```

```kotlin:SerializationTests.kt
package sample

import kotlinx.serialization.UpdateMode
import kotlinx.serialization.json.JSON
import kotlinx.serialization.list
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals

class SerializationTests {

    private lateinit var json: JSON

    @BeforeTest
    fun setUp() {
        json = JSON(
                unquoted = false,
                indented = false,
                indent = "    ",
                strictMode = true,
                updateMode = UpdateMode.OVERWRITE,
                encodeDefaults = true)
    }

    @Test
    fun testSerialize() {
        val jsonData = json.stringify(Data.serializer(), Data(42))
        assertEquals("""{"a":42,"b":"42"}""", jsonData)
        val jsonList = json.stringify(Data.serializer().list, listOf(Data(42)))
        assertEquals("""[{"a":42,"b":"42"}]""", jsonList)
    }

    @Test
    fun testDeserialize() {
        val obj = JSON.parse(Data.serializer(), """{"a":42}""")
        assertEquals(Data(a=42, b="42"), obj)
    }
}
```

まずは実行してみたいと思います。multiplatform plugin を使うと Gradle タスクに check, &lt;targetName&gt;Test が追加されます。つまり、今回のサンプルでは check, jvmTest, jsTest, macosTest タスクが追加されます。

> A test task is created under the name &lt;targetName&gt;Test for each target that is suitable for testing. Run the check task to run the tests for all targets.
>
> 引用：https://kotlinlang.org/docs/reference/building-mpp-with-gradle.html#running-tests

check タスクを使えば JVM, JS, Native の全てのターゲットでテストが実行される様です。

```shell
$ ./gradlew check
... snip ...
[==========] Running 2 tests from 1 test cases.
[----------] Global test environment set-up.
[----------] 2 tests from sample.SerializationTests
[ RUN      ] sample.SerializationTests.testSerialize
[       OK ] sample.SerializationTests.testSerialize (0 ms)
[ RUN      ] sample.SerializationTests.testDeserialize
[       OK ] sample.SerializationTests.testDeserialize (1 ms)
[----------] 2 tests from sample.SerializationTests (1 ms total)

[----------] Global test environment tear-down
[==========] 2 tests from 1 test cases ran. (1 ms total)
[  PASSED  ] 2 tests.
```

テストがパスした様子です。しかし、本当にテストされているのでしょうか？

## ターゲット毎にテストされていることを検証

ターゲット毎に結果の異なる簡単なテストを用意します。作成するファイルは SampleTests.kt です。

```
src
|- commonTest
|  |- kotlin
|     |- sample
|        |- SampleTests.kt
|- jvmTest
|  |- kotlin
|     |- sample
|        |- SampleTests.kt
|- jsTest
|  |- kotlin
|     |- sample
|        |- SampleTests.kt
|- macosTest
   |- kotlin
      |- sample
         |- SampleTests.kt
```

commonTest の SampleTests.kt にテストを用意します。isWorking 関数の戻り値が true であれば成功するテストです。

```kotlin:SampleTests.kt
package sample

import kotlin.test.Test
import kotlin.test.assertTrue

expect fun isWorking(): Boolean

class SampleTests {

    @Test
    fun testIsWorking() {
        assertTrue(isWorking())
    }
}
```

jvmTest, jsTest, macosTest では isWorking 関数を定義します。

```kotlin:SampleTests.kt
package sample

actual fun isWorking(): Boolean = true
```

check タスクを実行して確かめます。

```shell
$ ./gradlew check
... snip ...
[----------] 1 tests from sample.SampleTests
[ RUN      ] sample.SampleTests.testIsWorking
[       OK ] sample.SampleTests.testIsWorking (0 ms)
[----------] 1 tests from sample.SampleTests (0 ms total)
... snip ...
```

テストはパスします。次に、各ターゲットの isWorking 関数の戻り値を false にして check タスクを実行してみます。

| ターゲット | テスト結果 |
|:-:|:-:|
| jvm | FAILED |
| js | OK |
| macos | FAILED |

おや？ターゲットが js の場合は OK になります。js ではテストされていないことがわかりました。実はテスト実行は JVM, Android, Linux, Windows, macOS のみのサポートになっています。JS はサポートされていません。

> Running tests in a Gradle build is currently supported by default for JVM, Android, Linux, Windows and macOS; JS and other Kotlin/Native targets need to be manually configured to run the tests with an appropriate environment, an emulator or a test framework.
>
> 引用：https://kotlinlang.org/docs/reference/building-mpp-with-gradle.html#running-tests

## jsTest タスクの実行環境を構築する

[Multiplatform Kotlin application のサンプル](https://github.com/MarcinMoskala/KotAcademyPortal) を参考に、jsTest タスクを実行できる様にしてみます。

サンプルでは [Gradle Plugin for Node](https://github.com/srs/gradle-node-plugin) と [Mocha](https://mochajs.org/) を組み合わせて構築しています。しかし、Gradle Plugin for Node の更新は止まっており、node のダウンロードで失敗してしまいます。他の方法を探してみたところ、[Node Gradle Plugin](https://github.com/liferay/liferay-portal/tree/master/modules/sdk/gradle-plugins-node) を発見しました。

まずは、sample-library の build.gradle に Node Gradle Plugin の設定を追加します。

```groovy:build.gradle
buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("com.liferay:com.liferay.gradle.plugins.node:$nodePluginVersion")
    }
}

apply plugin: "com.liferay.node"

node {
    nodeVersion = project.nodeVersion
}
```

nodePluginVersion と nodeVersion は gradle.properties で設定しています。

```properties:gradle.properties
nodePluginVersion=4.5.1
nodeVersion=10.13.0
```

今回、node の設定はバージョンだけを行っています。バージョンは 10.13.0 (LTS) を指定しています。他にも諸々の設定があり、デフォルト設定では node がダウンロードされる設定になっています。インストール済みの node を使いたい場合には [Project Extension](https://github.com/liferay/liferay-portal/tree/master/modules/sdk/gradle-plugins-node#project-extension) で指定できます。

プラグインの設定に続いて、node module に関する設定を package.json に記述します。ファイルの置き場所は sample-library ディレクトリ直下です。

```json:package.json
{
  "name": "sample-library",
  "devDependencies": {
    "mocha": "5.2.0"
  },
  "scripts": {
    "test": "node_modules/mocha/bin/mocha node_modules"
  }
}
```

Mocha をインストールさせるために devDependencies を記述し、テスト実行のために scripts を記述します。ひとまず、node_modules 以下にあるテストコードを実行させます。

node_modules 以下にテストコードを配置することにしたので、node_modules ディレクトリにテストコードをコピーする設定を build.gradle に追加します。[サンプル](https://github.com/MarcinMoskala/KotAcademyPortal) でも同様の処理を行っています。但し、サンプルでは multiplatform Gradle plugin を使っておらず、細かい部分で変更を加える必要がありました。

```groovy:build.gradle
task populateNodeModules(dependsOn: [npmInstall, compileTestKotlinJs]) {
    doLast {
        copy {
            from compileKotlinJs.destinationDir
            from compileTestKotlinJs.destinationDir

            def jsCompilations = kotlin.targets.js.compilations
            jsCompilations.test.runtimeDependencyFiles.each {
                if (it.exists() && !it.isDirectory()) {
                    println("from ${it.toString().split('/').last()}")
                    from zipTree(it.absolutePath).matching { include '*.js' }
                }
            }

            into "${projectDir}/node_modules"
        }
    }
}
```

npmInstall タスクは Node Gradle plugin に含まれるタスクです。npmInstall タスクが完了すると node_modules ディレクトリに Node モジュールがインストールされている状態になります。今回のサンプルでは Mocha のインストールが npmInstall タスクで行われます。

また、compileTestKotlinJs タスクは multiplatform Gradle plugin に含まれるタスクです。targetName に応じた compileTestKotlin&lt;targetName&gt; タスクが生成されます。今回のサンプルでは、compileTestKotlinJs, compileTestKotlinJvm, compileTestKotlinMacos が生成されています。これらのタスクの存在は `println(tasks)` を実行して調べられます。

`jsCompilations` あたりのコードは [IDEA のコード](https://github.com/JetBrains/kotlin/blob/503f061c8c00c1b1a56148694ffbf54181b0e070/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/KotlinGradleWebMultiplatformModuleBuilder.kt#L289-L302) を参考にしています。このコードによって、依存しているライブラリのアーカイブから JavaScript ファイルを抽出しています。例えば、Kotlin 標準ライブラリの JAR ファイルから kotlin.js が抽出されます。

設定の完了まであと一息です。最後に、jsTest タスク実行前に Mocha が実行される様にし、Mocha の実行前に populateNodeModules が実行される様にします。また、モジュールの種類は node に対応する種類 (commonjs または umd) を指定します。

```groovy:build.gradle
[compileKotlinJs, compileTestKotlinJs]*.configure {
    kotlinOptions {
        sourceMap = true
        sourceMapEmbedSources = "always"
        moduleKind = 'commonjs'
    }
}

jsTest.dependsOn npmRunTest
npmRunTest.dependsOn populateNodeModules
```

npmRun&lt;scriptName&gt; タスクで Mocha を実行しています。package.json の scriptes で設定した test スクリプトを、npmRunTest タスクで実行できます。

以上で、jsTest タスクの実行環境の構築は完了です。jsTest タスクを実行して試してみるとテストの実行を確認できます。

# 複雑なデータ構造で JSON 変換を試す

環境が整ったところで、複雑なデータ構造で JSON 変換をしてみます。試すデータ構造は以下のとおりです。ComplexData オブジェクトが複数の ChildData オブジェクトを List として持ち、ChildData オブジェクトが BigDecimal と DateTime を持つデータ構造です。 

```kotlin:ComplexData.kt
package sample

import kotlinx.serialization.Serializable

@Serializable
data class ComplexData(val children: List<ChildData>) {
    constructor(vararg children: ChildData) : this(children.toList())
}
```

```kotlin:ChildData.kt
package sample

import com.soywiz.klock.DateTime
import kotlinx.serialization.Serializable

@Serializable
data class ChildData(
        val decimal: BigDecimal,
        val dateTime: DateTime
)
```

## BigDecimal

BigDecimal は expect と actual を使って以下に示すとおりに実装します。雑な実装なので、四則演算すらできません。actual class で data class を使おうと思ったら、expect class でプライマリコンストラクタにパラメータを持たせることになります。expect class のプライマリコンストラクタにはプロパティパラメータ (val/var が付くパラメータ) を持たせられないため、value パラメータには val/var は付けません。

```kotlin:commonMain/../BigDecimal.kt
package sample

expect class BigDecimal(value: String) : Comparable<BigDecimal>
```

```kotlin:jvmMain/../BigDecimal.kt
package sample

import java.math.BigDecimal as JvmBigDecimal

actual class BigDecimal actual constructor(value: String) : Comparable<BigDecimal> {
    private val value = JvmBigDecimal(value)
    override fun toString(): String = value.toString()
    override fun compareTo(other: BigDecimal): Int = value.compareTo(other.value)
}
```

```kotlin:jsMain/../BigDecimal.kt
package sample

actual data class BigDecimal actual constructor(private val value: String) : Comparable<BigDecimal> {
    override fun toString(): String = value
    override fun compareTo(other: BigDecimal): Int = compareValues(value, other)
}
```

macosMain の BigDecimal.kt は jsMain の BigDecimal.kt と同じにします。実際にはそれぞれのプラットフォームで異なる実装になるでしょう。

マルチプラットフォーム対応のライブラリが無い場合には、各プラットフォームのライブラリをラッピングして使用したり、独自に実装する必要がでてくると思います。もっと良い方法があれば知りたいところです。

## DateTime

DateTime にはマルチプラットフォーム対応の [Klock](https://github.com/korlibs/klock) というライブラリがあります。Klock がどれだけ使い物になるかは未知数です。今回は動かしてみることが目的なので使ってみました。

gradle.properties に Klock のバージョンを追加します。

```properties:gradle.properties
klockVersion=1.0.0
```

次に build.gradle の commonMain に Klock への依存を追加します。リポジトリの指定も必要です。以下には追記部分のみを記述しています。

```groovy:build.gradle
repositories {
    // for DateTime
    maven { url "https://dl.bintray.com/soywiz/soywiz" } // 追記
}

kotlin {
    sourceSets {
        commonMain {
            dependencies {
                implementation "com.soywiz:klock:$klockVersion" // 追記
            }
        }
    }
}
```

## テストコード

動作確認するためのテストコードでは以下を行っています。

1. ComplexData オブジェクトを JSON に変換
2. JSON を ComplexData オブジェクトに変換
3. 前後の ComplexData オブジェクトを比較

```kotlin:commonTest/../ComplexSerializationTests.kt
package sample

import com.soywiz.klock.DateTime
import com.soywiz.klock.days
import kotlinx.serialization.UpdateMode
import kotlinx.serialization.json.JSON
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals

class ComplexSerializationTests {

    private lateinit var json: JSON

    @BeforeTest
    fun setUp() {
        json = JSON(
                unquoted = false,
                indented = false,
                indent = "    ",
                strictMode = true,
                updateMode = UpdateMode.OVERWRITE,
                encodeDefaults = true)
    }

    @Test
    fun testComplexDate() {
        val data = ComplexData(
                ChildData(
                        BigDecimal("12345678901234567890123456789012345678901234567890"),
                        DateTime.now()
                ),
                ChildData(
                        BigDecimal("12345678901234567890123456789012345678901234567890.09876543210987654321"),
                        DateTime.now() + 1.days
                )
        )
        val json = JSON.stringify(ComplexData.serializer(), data)
        println(json)
        val deserializedData = JSON.parse(ComplexData.serializer(), json)
        assertEquals(data, deserializedData)
    }
}
```

## テストの実行結果

テストを実行します。

```bash
$ ./gradlew check
...
SerializationException: Can't locate argument-less serializer for class BigDecimal. For generic classes, such as lists, please provide serializer explicitly.
...
BUILD FAILED in 1s
1 actionable task: 1 executed
```

残念ながら失敗します。serializer を明示する様に書かれています。それもそうです。BigDecimal と DateTime では Serializer を生成していません。

BigDecimal クラスに Serializable アノテーションを付けて再度テストを実行してみます。

```bash
$ ./gradlew check
...
    TypeError: tmp$.serializer is not a function
      at compiledSerializer (node_modules/kotlinx-serialization-runtime-js.js:8711:87)
...
BUILD FAILED in 2s
5 actionable tasks: 3 executed, 2 up-to-date
```

serializer が関数ではないと言われています。困りました。

[Multiplatform Kotlin application のサンプル](https://github.com/MarcinMoskala/KotAcademyPortal) では expect, actual を使いつつ serialization を使っています。そこでは、Serializer をカスタマイズすることで実現しています。

## Serializer のカスタマイズ

BigDecimal のカスタム Serializer を作ってみます。

```kotlin:commonMain/../BigDecimalSerializer.kt
package sample

import kotlinx.serialization.Decoder
import kotlinx.serialization.Encoder
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializer

@Serializer(forClass = BigDecimal::class)
object BigDecimalSerializer: KSerializer<BigDecimal> {

    override fun serialize(output: Encoder, obj: BigDecimal) {
        output.encodeString(obj.toString())
    }

    override fun deserialize(input: Decoder): BigDecimal {
        return BigDecimal(input.decodeString())
    }
}
```

DateTime はライブラリに含まれるクラスです。このようなクラスには Serializable アノテーションを付与できないため、こちらもカスタム Serializer を作成します。

```kotlin:commonMain/../DateTimeSerializer.kt
package sample

import com.soywiz.klock.DateFormat
import com.soywiz.klock.DateTime
import com.soywiz.klock.parse
import kotlinx.serialization.Decoder
import kotlinx.serialization.Encoder
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializer

@Serializer(forClass = DateTime::class)
object DateTimeSerializer : KSerializer<DateTime> {

    private val format = DateFormat("yyyy-MM-dd'T'hh:mm:ss")

    override fun serialize(output: Encoder, obj: DateTime) {
        output.encodeString(obj.format(format))
    }

    override fun deserialize(input: Decoder): DateTime {
        return format.parse(input.decodeString()).utc
    }
}
```

## 再度テストを実行

では、再びテストを実行します。

```bash
./gradlew check
...
Caused by: java.lang.IllegalStateException: Class DateTime is not externally serializable
        at org.jetbrains.kotlinx.serialization.compiler.backend.common.SerializerCodegen.generate(SerializerCodegen.kt:42)
...
BUILD FAILED in 1s
1 actionable task: 1 executed
```

再び残念な結果になりました。

エラーは [SerializerCodegen.kt](https://github.com/JetBrains/kotlin/blob/96bf749d4d2ec5a07b44d0ae10e5ab2b2459c8a5/plugins/kotlin-serialization/kotlin-serialization-compiler/src/org/jetbrains/kotlinx/serialization/compiler/backend/common/SerializerCodegen.kt#L42) で properties.isExternallySerializable のチェックに引っかかることで発生しています。[SerializableProperties.kt](https://github.com/JetBrains/kotlin/blob/96bf749d4d2ec5a07b44d0ae10e5ab2b2459c8a5/plugins/kotlin-serialization/kotlin-serialization-compiler/src/org/jetbrains/kotlinx/serialization/compiler/resolve/SerializableProperties.kt#L34) に isExternallySerializable の実装があります。

```kotlin:SerializableProperties.kt
    val isExternallySerializable: Boolean =
        primaryConstructorParameters.size == primaryConstructorProperties.size
```

プライマリコンストラクタのパラメータとプロパティの数が一致する必要があるとのことです。DateTime クラスの実装を見ると、プライマリコンストラクタにはプロパティが一つだけなので問題なさそうに見えますが inline class になっていることが問題なのかもしれません。[Class <ClassName> is not externally serializable についての Issue](https://github.com/Kotlin/kotlinx.serialization/issues/129) が上がっており、[v1.3.20-eap-25 の SerializerCodegen.kt](https://github.com/JetBrains/kotlin/blob/b4101ef2aec389c11c3001084b1a723316b7c014/plugins/kotlin-serialization/kotlin-serialization-compiler/src/org/jetbrains/kotlinx/serialization/compiler/backend/common/SerializerCodegen.kt#L44-L58) を見ると実装が変更されているため、次のバージョンを待ってみることにします。

ということで DateTime は断念することにして、BigDecimal だけでも動かしてみたいと思います。ChildData から DateTime を削除し、関連するコードも全て削除した上でテストを再々実行すると、やはり失敗します。

## Serializer の指定

エラーメッセージには please provide serializer explicitly. と書かれています。Serializer を明示的に指定しなければいけない様子なので、以下のとおりに指定します。

```kotlin:commonMain/../ChildData.kt

package sample

import kotlinx.serialization.Serializable

@Serializable
data class ChildData(
        @Serializable(with = BigDecimalSerializer::class) val decimal: BigDecimal
)
```

以下の様に追加する方法でいけるかと思ったのですが駄目でした。

```kotlin:ComplexSerializationTests.kt
        json.install(SimpleModule(BigDecimal::class, BigDecimalSerializer))
```

## ３度目の正直？

今回は JS でのテストには成功します！しかし、JVM では IllegalStateException: Class BigDecimal is not externally serializable が発生します。これは既に原因を追求したエラーです。次のバージョンまで待つ事案として保留します。

# おわりに

今回は BigDecimal, DateTime を含むサンプルは動かせられず、かなり残念な結果になってしまいました。まだバージョン 1.0 になっていないので仕方がないかなとは思います。冒頭に掲げた以下の構成を実現する道のりは、まだまだ遠いのかなと思います。

<img src="https://docs.google.com/drawings/d/e/2PACX-1vSDi0jO2Nzf4RqHnX3yPSminOAXefnF_Ufuu1RhnYY1mHA8ohQTTXW9KsP2RplGza7PMyIZKx0qRto9/pub?w=960&amp;h=720">

上の構成を実現するには JSON 変換だけでなく HTTP クライアントライブラリと非同期処理ライブラリの共通化も必要です。

HTTP クライアントライブラリは、[Ktor](https://ktor.io/clients/http-client/multiplatform.html) で Multiplatform Http Client の開発が進んでいます。非同期処理ライブラリとしてはコルーチンが既にマルチプラットフォーム対応しています。[RxKotlin](https://github.com/ReactiveX/RxKotlin/issues/115) や [Reactor](https://github.com/reactor/reactor-core/issues/979) でもマルチプラットフォーム対応の話が挙がってはいる様子です。

今回やってみて最も良さそうだなと思った点は、テストコードを共通化できる点です。複数のプラットフォーム毎に通信部分の実装とテストを行うのではなく、ひとつのテストで複数のプラットフォーム対応にできたら随分と楽なのではないかと思いました。

また来年も何かを試してみたいと思います。

# おまけ

下記の記事なんかも読んでみては如何でしょうか？

- [Kotlin Multiplatform構想 ~今やる理由編~ (11日目の代打記事)](https://aakira.app/blog/2018/12/kotlin-mpp-reason/)
- [Xamarin 使いが Kotlin のマルチプラットフォーム対応コードを読んだ感想](https://qiita.com/amay077/items/45b1ad4b9c5d3a03cf9c)
- [Everything in Kotlinについて考える①](https://qiita.com/44x1carbon/items/643e944ae0d9d1da9325)
- [kotlinx.serializationメモ](https://qiita.com/44x1carbon/items/63d7e5f1a33be3f108d5)