Home About Contact
Kotlin , Kotlin Multiplatform , JSON , JavaScript , webpack

Kotlin Multiplatform で Kotlin/JS するときに JSON を扱う

org.json で JSON 操作するコードを書いていたが Kotlin/JS ではこれは使えない。 そのかわりに Serialization JSON を使った。

以前のメモ

Serialization の場合、データクラスを JSON にしたり、逆にJSON から特定のデータクラスに戻したりといったことが簡単にできる。 でも結局変換コードかかないといけないんでしょ?と思ったら String とか List<String> などの型ならば、 特別なコードを書く必要はなく encode / decode できた。これはよい。

環境

Gradle:

$ gradle --version

------------------------------------------------------------
Gradle 8.4
------------------------------------------------------------

Build time:   2023-10-04 20:52:13 UTC
Revision:     e9251e572c9bd1d01e503a0dfdf43aedaeecdc3f

Kotlin:       1.9.10
Groovy:       3.0.17
Ant:          Apache Ant(TM) version 1.10.13 compiled on January 4 2023
JVM:          17.0.12 (Ubuntu 17.0.12+7-Ubuntu-1ubuntu222.04)
OS:           Linux 6.8.0-40-generic amd64

Node.js:

$ node --version
v20.17.0

hello プロジェクト

$ mkdir hello
$ cd hello
$ touch build.gradle.kts

build.gradle.kts に次のように記述:

plugins {
    kotlin("multiplatform") version "2.0.0"
    kotlin("plugin.serialization") version "2.0.20"
}

version = "0.1"

repositories {
    mavenCentral()
}

kotlin {
    js {
        nodejs()
        binaries.library()
    }

    sourceSets {
        jsMain.dependencies {
            implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.2")
        }
    }
}

ポイントは plugins セクションにこれを記述し:

plugins {
    ...
    kotlin("plugin.serialization") version "2.0.20"
}

jsMain の依存として kotlinx-serialization-json を指定します。

kotlin {
    ... 

    sourceSets {
        jsMain.dependencies {
            implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.2")
        }
    }
}

続いて Main.kt の用意:

$ mkdir -p src/jsMain/kotlin/
$ touch src/jsMain/kotlin/Main.kt

内容は次のようにします。

// Main.kt

import kotlinx.serialization.json.Json
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.decodeFromString

typealias JSON = String

@Serializable
data class Pokemon(val name: String)

@Serializable
data class Trainer(
    val name: String,
    val pokemons: List<Pokemon>)


@ExperimentalJsExport
@JsExport
fun buildTrainerJson(trainerName: String, vararg pokemonNames: String): JSON {
    val pokemons = pokemonNames.map { Pokemon(it) }
    val trainer  = Trainer(trainerName, pokemons)
    return Json.encodeToString( trainer )
}

@ExperimentalJsExport
@JsExport
fun getTrainerNameAndPokemonNames(json: JSON): String {
    val trainer  = Json.decodeFromString<Trainer>( json )
    val trainerName = trainer.name
    val pokemonNames = trainer.pokemons.map { it.name }.joinToString(", ")
    return "$trainerName: ($pokemonNames)"
}

この部分、複数のポケモン名を渡す部分は vararg を使用しました。

fun buildTrainerJson(trainerName: String, vararg pokemonNames: String): JSON {

はじめ、 List<String> としてこのように記述していたのですが、意図通り作動しませんでした。

fun buildTrainerJson(trainerName: String, pokemonNames: List<String>): JSON {
$ gradle assemble

エラーがなければ gradle wrapper しておきます。

$ gradle wrapper

それでは build します。

$ ./gradlew build

これで build/js に Node.js のプロジェクトが生成されます。

build/js/index.js を次の内容で用意します。

const hello = require("hello")
const json = hello.buildTrainerJson("Satoshi", ["Pikachu", "Squirtle"])
console.log(json)

const value = hello.getTrainerNameAndPokemonNames(json)
console.log(value)

それでは実行してみます。

$ cd build/js
$ node index.js
{"name":"Satoshi","pokemons":[{"name":"Pikachu"},{"name":"Squirtle"}]}
Satoshi: (Pikachu, Squirtle)

うまくいきました。

ライブラリ hellolib.js を作成

$ npm install webpack@5.92.0 --save-dev
$ npm install webpack-cli@5.1.4 --save-dev

webpack.config.js を用意。

const path = require('path');

module.exports = {
    mode: 'production',
    entry: './packages/hello/kotlin/hello.js',
    output: {
        library: 'hello',
        libraryTarget: 'commonjs',
        //libraryTarget: 'this',
        path: __dirname,
        filename: 'hellolib.js'
    }
}

詳細は割愛しますが、 libraryTargetthis を指定すると、ブラウザ上で直接実行する js として使えます。

$ npx webpack --config webpack.config.js

hellolib.js が成果物としてのライブラリです。

作動を確認します。

// main.js
const hellolib = require("./hellolib.js")
console.log(hellolib)

const json = hellolib.hello.buildTrainerJson("Satoshi", ["Pikachu", "Squirtle"])
console.log(json)

const value = hellolib.hello.getTrainerNameAndPokemonNames(json)
console.log(value)

実行。

$ node main.js
{
  hello: {
    buildTrainerJson: [Function: D],
    getTrainerNameAndPokemonNames: [Function: O]
  }
}
{"name":"Satoshi","pokemons":[{"name":"Pikachu"},{"name":"Squirtle"}]}
Satoshi: (Pikachu, Squirtle)

できました。

まとめ

Kotlin/JS する場合、JavaScript 側とのやりとりで JSON を使うことが多いので、Serialization 便利です。