kotlin-serializationメモ
kotlin-serialization キツすぎ。ていうかそもそも Android のすべてがキツい。
意味不明すぎたので新しい Android アプリプロジェクトを作ってテストを書いてみるかと思った。
これを見てとりあえず build.gradle をいじろうと思ったけど、Project の build.gradle と App module の build.gradle のどちらに書けば良いのかわからない。俺の理解だと別にどっちに書いても動くと思うんだけど、実際問題 App module の build.gradle に書いても Sync 時にエラーが出る。
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 の知識を網羅しないといけないんだ。興味なさすぎる。
そもそも一緒に指定する必要がある org.jetbrains.kotlin.jvm
は一体何なんだ?と思ったけど情報量ゼロで笑ってしまった。
こっちと何が違うわけ?
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.android
を apply 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.application
を apply 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 が同じプロジェクト内部で使えると都合が悪いのは想像はできる。
この辺に書いてある。 Error resolving plugin
なので、Resolving step で壊れてるよという意味なのは理解できる。ググっても全然このドキュメントにはたどり着けなかったけど。
classpath って何?で調べたらここにたどり着いたが、肝心の classpath が何なのかはわからなかった。何?
要するにどういうこと?
Project gradle.build は「プロジェクト全体にまつわる設定を書く」と説明されるけど、その具体的な利用ケースとして「個々の module の gradle.build 間で共同で使いたい Gradle Plugin への依存を定義する」があるという理解をしておくと良いのかもしれない。
ていうかそもそも Project と Module の関係ってどうなってるの?
kotlin-serialization
のドキュメントに org.jetbrains.kotlin.jvm
について記述があるってことは、これが必要ってことだろう。org.jetbrains.kotlin.jvm
は情報が全然なかったけど、調べてると以下のセクションを見つけた。
要するに Kotlin を JVM 向けにビルドするために必要ってこと? なんで kotlin-serialization がこれを必要としてるのか全然わからないけど。
試しに消してみた。
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
}
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id "org.jetbrains.kotlin.plugin.serialization"
}
ビルドできた。JVMは必須ではないっぽい。
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については何もテストしていないが)
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 に同じような動きをするものはないんだろうか。
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 }
}
}
本体。壊れるケースがあるかもしれない。