Open8

kotlin-serializationメモ

NiaNia

kotlin-serialization キツすぎ。ていうかそもそも Android のすべてがキツい。

NiaNia

意味不明すぎたので新しい Android アプリプロジェクトを作ってテストを書いてみるかと思った。

https://kotlinlang.org/docs/serialization.html#example-json-serialization

これを見てとりあえず build.gradle をいじろうと思ったけど、Project の build.gradle と App module の build.gradle のどちらに書けば良いのかわからない。俺の理解だと別にどっちに書いても動くと思うんだけど、実際問題 App module の build.gradle に書いても Sync 時にエラーが出る。

app/build.gradle
plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id 'org.jetbrains.kotlin.jvm' version '1.7.20'
    id 'org.jetbrains.kotlin.plugin.serialization' version '1.7.20'
}
Build file '$HOME/KotlinSerializableTest/app/build.gradle' line: 4

Error resolving plugin [id: 'org.jetbrains.kotlin.jvm', version: '1.7.20']
> Plugin request for plugin already on the classpath must not include a version

versionを消すと、

Build file '/Users/shota-nagasaki/KotlinSerializableTest/app/build.gradle' line: 5

Plugin [id: 'org.jetbrains.kotlin.plugin.serialization'] was not found in any of the following sources:

Project の build.gradle に入れたら通るんだけど、なんでそうしないと通らないのかが全くわからない。これってスコープが違うだけじゃないの?

ちょっと調べたけど全然わからなかった。Gradle ちゃんと知識がないと使えないので使いたくない。Android のコードを書くためになんで Gradle の知識を網羅しないといけないんだ。興味なさすぎる。

NiaNia

1. Project gradle.build にだけ plugin を書く

試しに org.jetbrains.kotlin.android を Project gradle.build にだけ書いて、App module gradle.build から削除してみた。すると以下のエラーが出た。

A problem occurred evaluating project ':app'.
> No signature of method: build_v8gd6oua6lz7prn0yix90k5m.android() is applicable for argument types: (build_v8gd6oua6lz7prn0yix90k5m$_run_closure1) values: [build_v8gd6oua6lz7prn0yix90k5m$_run_closure1@489ff371]

android() というメソッドが存在しないと言っている。Project gradle.build で apply されていないので、android() メソッドが存在しないってことだろう。

次に Project gradle.build の org.jetbrains.kotlin.androidapply true してみたが、以下のエラーが出た。

A problem occurred configuring root project 'KotlinSerializableTest'.
> 'kotlin-android' plugin requires one of the Android Gradle plugins.
  Please apply one of the following plugins to ':' project:
  - com.android.application
  	- com.android.library
  	- com.android.dynamic-feature
  	- com.android.test
  	- com.android.instantapp
  	- com.android.feature

org.jetbrains.kotlin.android は他の Gradle Plugin を必要としているらしい。適当に com.android.applicationapply true したらビルドは通ったけど、相変わらず android() が見つからないと言ってくる。結局、App module gradle.build 内部で使いたい Gradle Plugin は App module gradle.build の plugins Script Block 内部にちゃんと書かないとだめってことみたい。

2. App module gradle.build にだけ plugin を書く

デフォルトだと以下

plugins {
    id 'com.android.application'
    id "org.jetbrains.kotlin.android"
}

Project gradle.build から org.jetbrains.kotlin.android を削除した場合、以下のように version を指定することができるようになる。

plugins {
    id 'com.android.application'
    id "org.jetbrains.kotlin.android" version "1.7.10"
}

Project gradle.build から org.jetbrains.kotlin.android を削除していない場合、以下のエラーが出る

Error resolving plugin [id: 'org.jetbrains.kotlin.android', version: '1.7.20']
> Plugin request for plugin already on the classpath must not include a version

よくわからんけど plugin は classpath というところに入って、すでに classpath に入っている plugin を指定することはできるが、その場合は version を指定してはいけないという感じなんだろう。バージョン違いの plugin が同じプロジェクト内部で使えると都合が悪いのは想像はできる。

https://docs.gradle.org/current/userguide/plugins.html#sec:using_plugins

この辺に書いてある。 Error resolving plugin なので、Resolving step で壊れてるよという意味なのは理解できる。ググっても全然このドキュメントにはたどり着けなかったけど。

classpath って何?で調べたらここにたどり着いたが、肝心の classpath が何なのかはわからなかった。何?

要するにどういうこと?

Project gradle.build は「プロジェクト全体にまつわる設定を書く」と説明されるけど、その具体的な利用ケースとして「個々の module の gradle.build 間で共同で使いたい Gradle Plugin への依存を定義する」があるという理解をしておくと良いのかもしれない。

ていうかそもそも Project と Module の関係ってどうなってるの?

NiaNia

kotlin-serialization のドキュメントに org.jetbrains.kotlin.jvm について記述があるってことは、これが必要ってことだろう。org.jetbrains.kotlin.jvm は情報が全然なかったけど、調べてると以下のセクションを見つけた。

https://kotlinlang.org/docs/gradle.html#targeting-the-jvm

要するに Kotlin を JVM 向けにビルドするために必要ってこと? なんで kotlin-serialization がこれを必要としてるのか全然わからないけど。

試しに消してみた。

gradle.build (Project)
plugins {
    id 'com.android.application' version '7.2.2' apply false
    id 'com.android.library' version '7.2.2' apply false
    id 'org.jetbrains.kotlin.android' version '1.7.20' apply false
    id "org.jetbrains.kotlin.plugin.serialization" version '1.7.20' apply false
}
gradle.build (App module)
plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id "org.jetbrains.kotlin.plugin.serialization"
}

ビルドできた。JVMは必須ではないっぽい。

NiaNia
package com.niaeashes.soil.kotlin_serializable

import org.junit.Test

import org.junit.Assert.*
import kotlinx.serialization.*
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.encodeToJsonElement

@Serializable
data class Account(
    @Serializable(with = IgnoreUnknownListSerializer::class)
    val permissions: List<PermissionValue>
)

@Serializable
enum class PermissionValue {
    @SerialName("test") TEST,
    @SerialName("debug") DEBUG
}

class OptionalSerializer<T : Any>(private val valueSerializer: KSerializer<T?>) : KSerializer<T?> {
    override val descriptor: SerialDescriptor = valueSerializer.descriptor

    override fun serialize(encoder: Encoder, value: T?) {
        valueSerializer.serialize(encoder, value)
    }

    override fun deserialize(decoder: Decoder): T? {
        return try {
            return valueSerializer.deserialize(decoder)
        } catch (ex: Exception) {
            return null
        }
    }
}

class IgnoreUnknownListSerializer<E: Any>(valueSerializer: KSerializer<E?>): KSerializer<List<E>> {
    private val serializer = ListSerializer(OptionalSerializer(valueSerializer))

    override val descriptor: SerialDescriptor = serializer.descriptor

    override fun deserialize(decoder: Decoder): List<E> {
        return serializer.deserialize(decoder).mapNotNull { it }
    }

    override fun serialize(encoder: Encoder, value: List<E>) {
        serializer.serialize(encoder, value as List<E?>)
    }
}

class ExampleUnitTest {
    @Test
    fun emptyPermissions() {
        val account = Json.decodeFromString<Account>("""{ "permissions": [] }""")
        assertEquals(listOf<PermissionValue>(), account.permissions)
        val json = Json.encodeToJsonElement(account)
        assertEquals("""{"permissions":[]}""", json.toString())
    }
    @Test
    fun testPermissions() {
        val account = Json.decodeFromString<Account>("""{ "permissions": ["test"] }""")
        assertEquals(listOf(PermissionValue.TEST), account.permissions)
        val json = Json.encodeToJsonElement(account)
        assertEquals("""{"permissions":["test"]}""", json.toString())
    }
    @Test
    fun invalidPermissions() {
        val account = Json.decodeFromString<Account>("""{ "permissions": ["invalid"] }""")
        assertEquals(listOf<PermissionValue>(), account.permissions)
        val json = Json.encodeToJsonElement(account)
        assertEquals("""{"permissions":[]}""", json.toString())
    }
}

最終的にやりたかったことはテストできた(serializeについては何もテストしていないが)

NiaNia
package com.niaeashes.soil.kotlin_serializable

import org.junit.Test

import org.junit.Assert.*
import kotlinx.serialization.*
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.encodeToJsonElement

@Serializable
data class Account(
    @Serializable(with = IgnoreUnknownListSerializer::class)
    val permissions: List<PermissionValue>
)

@Serializable
enum class PermissionValue {
    @SerialName("test") TEST,
    @SerialName("debug") DEBUG
}

class OptionalSerializer<T : Any>(private val valueSerializer: KSerializer<T?>) : KSerializer<T?> {
    override val descriptor: SerialDescriptor = valueSerializer.descriptor

    override fun serialize(encoder: Encoder, value: T?) {
        valueSerializer.serialize(encoder, value)
    }

    override fun deserialize(decoder: Decoder): T? {
        return try {
            return valueSerializer.deserialize(decoder)
        } catch (ex: Exception) {
            return null
        }
    }
}

class IgnoreUnknownListSerializer<E: Any>(valueSerializer: KSerializer<E?>): KSerializer<List<E>> {
    private val serializer = ListSerializer(OptionalSerializer(valueSerializer))

    override val descriptor: SerialDescriptor = serializer.descriptor

    override fun deserialize(decoder: Decoder): List<E> {
        return serializer.deserialize(decoder).mapNotNull { it }
    }

    override fun serialize(encoder: Encoder, value: List<E>) {
        serializer.serialize(encoder, value as List<E?>)
    }
}

class ExampleUnitTest {
    @Test
    fun emptyPermissions() {
        val account = Json.decodeFromString<Account>("""{ "permissions": [] }""")
        assertEquals(listOf<PermissionValue>(), account.permissions)
        val json = Json.encodeToJsonElement(account)
        assertEquals("""{"permissions":[]}""", json.toString())
    }
    @Test
    fun testPermissions() {
        val account = Json.decodeFromString<Account>("""{ "permissions": ["test"] }""")
        assertEquals(listOf(PermissionValue.TEST), account.permissions)
        val json = Json.encodeToJsonElement(account)
        assertEquals("""{"permissions":["test"]}""", json.toString())
    }
    @Test
    fun invalidPermissions() {
        val account = Json.decodeFromString<Account>("""{ "permissions": ["invalid"] }""")
        assertEquals(listOf<PermissionValue>(), account.permissions)
        val json = Json.encodeToJsonElement(account)
        assertEquals("""{"permissions":[]}""", json.toString())
    }
}

serialize についてもテストしたバージョン。正常に動いている。

てか OptionalSerializer 定義してるけど builtins に同じような動きをするものはないんだろうか。

NiaNia
class IgnoreUnknownListSerializer<E: Any>(valueSerializer: KSerializer<E?>): KSerializer<List<E>> {
    private val serializer = ListSerializer(ElementSerializer(valueSerializer))

    override val descriptor: SerialDescriptor = serializer.descriptor

    override fun deserialize(decoder: Decoder): List<E>
        = serializer.deserialize(decoder).mapNotNull { it }

    override fun serialize(encoder: Encoder, value: List<E>)
        = serializer.serialize(encoder, value as List<E?>)

    class ElementSerializer<T : Any>(private val valueSerializer: KSerializer<T?>) : KSerializer<T?> {
        override val descriptor: SerialDescriptor = valueSerializer.descriptor

        override fun serialize(encoder: Encoder, value: T?)
            = valueSerializer.serialize(encoder, value)

        override fun deserialize(decoder: Decoder): T?
            = try { valueSerializer.deserialize(decoder) } catch (ex: Throwable) { null }
    }
}

本体。壊れるケースがあるかもしれない。