kotlinx.serializationに限らずオブジェクトマッパー系のパーサーで扱いにくいのが、状態によってフォーマットが変化してしまうデータです。そういった場合のためにパース処理をカスタマイズする方法が用意されているのです。ただし、kotlinx.serializationでは、「パースした結果をnullとして扱いたい」という要求がある場合、ちょっと複雑な実装が必要になってきます。
一例として、通常Objectなのだけど、データがない場合にnullではなく、空配列になるというJSONがあったとします。
{
"data": {
"title": "title"
}
}
titleの入ったdataが「ない」場合、
{
"data": null
}
の場合は、nullableと宣言すれば良いです。
{}
の場合は、デフォルト値を定義しておけばパース可能です。
以下のように定義すればどちらも対応できますね。
@Serializable
data class Data(
@SerialName("data")
val data: TitleData? = null,
) {
@Serializable
data class TitleData(
@SerialName("title")
val title: String,
)
}
しかし、
{
"data": []
}
と、空配列になってしまうデータがあったとします。kotlinx.serializationではojbectが配列に変化するデータはどのように扱えば良いでしょうか?
Moshiの場合はアノテーションを定義して、それに対応するJsonAdapterを定義することでパースできました。
Arrayとしてパースする
先に書いたようにkotlinx.serializationではパース結果をnullに差し替えるのが少し難しいので、nullではなくemptyにパースしてしまおうという作戦です。
公式ドキュメントで Array wrapping として紹介されている、単一データの場合にObjectになるArray、これが使えますね。
@Serializable
data class Data(
@SerialName("data")
@Serializable(with = TitleDataSerializer::class)
val data: List<TitleData>? = null,
) {
@Serializable
data class TitleData(
@SerialName("title")
val title: String,
)
}
object TitleDataSerializer: JsonTransformingSerializer<List<TitleData>>(ListSerializer(TitleData.serializer())) {
override fun transformDeserialize(element: JsonElement): JsonElement =
if (element !is JsonArray) JsonArray(listOf(element)) else element
}
Arrayとして受け取り、objectの場合は、単一要素のArrayに変換してからパースする作戦です。
nullableなListでデフォルト値をnullとしておけば、nullを指定されたり、フィールドが存在しない場合はnullになります。
本来扱いたい型とは異なってしまうので、その型でないと困る場合は、この結果を再変換しないといけないです。ちょっと効率が悪いですね。
とはいえこういう場合、JSONのデータ構造自体が本来扱いたいデータ構造とも異なっていると思いますので、それをserializerの工夫だけでうまく変換するのは、不可能か、可能であっても非常に複雑な実装が必要だったりするのでこれで割り切ってしまうのもありでしょう。
NullableJsonTransformingSerializerを作る
前項で紹介したJsonTransformingSerializerの型をTitleData?
にできれば本来やりたかった変換が簡単に実装できそうなのですが、残念ながらJsonTransformingSerializerの型は<T : Any>
になっているので、Nullableにできないです。
ならNullableにしちゃえば・・・・・・ということでソースコードを見てみます。以下のような実装になっていました。
public abstract class JsonTransformingSerializer<T : Any>(
private val tSerializer: KSerializer<T>
) : KSerializer<T> {
override val descriptor: SerialDescriptor get() = tSerializer.descriptor
final override fun serialize(encoder: Encoder, value: T) {
val output = encoder.asJsonEncoder()
var element = writeJson(output.json, value, tSerializer)
element = transformSerialize(element)
output.encodeJsonElement(element)
}
final override fun deserialize(decoder: Decoder): T {
val input = decoder.asJsonDecoder()
val element = input.decodeJsonElement()
return input.json.decodeFromJsonElement(tSerializer, transformDeserialize(element))
}
protected open fun transformDeserialize(element: JsonElement): JsonElement = element
protected open fun transformSerialize(element: JsonElement): JsonElement = element
}
serializeの方がinternalなメソッド、クラスが使われているのでカスタムするのが難しそうです。
今回必要なのはdesirializeの方なので割り切ってしまいましょう。なお、単純にNullableなserializerは、拡張フィールドnullable
を使うことで取得できます。これに処理を委譲してしまえば簡単に実装できます。
@OptIn(ExperimentalSerializationApi::class)
abstract class NullableJsonTransformingSerializer<T : Any>(
nonNullSerializer: KSerializer<T>,
) : KSerializer<T?> {
private val serializer: KSerializer<T?> = nonNullSerializer.nullable
override val descriptor: SerialDescriptor get() = serializer.descriptor
override fun serialize(encoder: Encoder, value: T?) {
serializer.serialize(encoder, value)
}
override fun deserialize(decoder: Decoder): T? {
val input = decoder as JsonDecoder
val element = input.decodeJsonElement()
return input.json.decodeFromJsonElement(serializer, transformDeserialize(element))
}
protected open fun transformDeserialize(element: JsonElement): JsonElement = element
}
これを使って以下のような実装をして
object TitleDataSerializer : NullableJsonTransformingSerializer<TitleData>(TitleData.serializer()) {
override fun transformDeserialize(element: JsonElement): JsonElement =
if (element is JsonArray) JsonNull else element
}
データクラスの方でこのSerializerを指定すればパースできます。
@Serializable
data class Data(
@SerialName("data")
@Serializable(with = TitleDataSerializer::class)
val data: TitleData? = null,
) {
@Serializable
data class TitleData(
@SerialName("title")
val title: String,
)
}
所望の動作にはなりましたが、割り切りが必要だし、なんだか遠回りな感じですね。
ライブラリがNullableJsonTransformingSerializerを用意してくれていればこの方法で良いのですが
専用SerialiserのAbstractクラスを作る
前項でJsonTransformingSerializerをNullableにカスタムしましたが、それを継承した今回の目的のための実装は非常に小さいので、継承時に必要な実装は個別の型情報だけになるように、専用のAbstractクラスを作ってみます。
前項のJsonTransformingSerializerと、その内部で使ったnullable
拡張フィールドの実装を参考にします。
nullable
の実装は以下のようになっています。
@OptIn(ExperimentalSerializationApi::class)
public val <T : Any> KSerializer<T>.nullable: KSerializer<T?>
get() {
@Suppress("UNCHECKED_CAST")
return if (descriptor.isNullable) (this as KSerializer<T?>) else NullableSerializer(this)
}
ここで使われているNullableSerializerの実装は以下、
@PublishedApi
@OptIn(ExperimentalSerializationApi::class)
internal class NullableSerializer<T : Any>(private val serializer: KSerializer<T>) : KSerializer<T?> {
override val descriptor: SerialDescriptor = SerialDescriptorForNullable(serializer.descriptor)
override fun serialize(encoder: Encoder, value: T?) {
if (value != null) {
encoder.encodeNotNullMark()
encoder.encodeSerializableValue(serializer, value)
} else {
encoder.encodeNull()
}
}
override fun deserialize(decoder: Decoder): T? {
return if (decoder.decodeNotNullMark()) decoder.decodeSerializableValue(serializer) else decoder.decodeNull()
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as NullableSerializer<*>
if (serializer != other.serializer) return false
return true
}
override fun hashCode(): Int {
return serializer.hashCode()
}
}
@OptIn(ExperimentalSerializationApi::class)
internal class SerialDescriptorForNullable(
internal val original: SerialDescriptor
) : SerialDescriptor by original, CachedNames {
override val serialName: String = original.serialName + "?"
override val serialNames: Set<String> = original.cachedSerialNames()
override val isNullable: Boolean
get() = true
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is SerialDescriptorForNullable) return false
if (original != other.original) return false
return true
}
override fun toString(): String {
return "$original?"
}
override fun hashCode(): Int {
return original.hashCode() * 31
}
}
ということで、空配列になった時、nullとしてデコードするEmptyAsNullSerializer
というクラスを作ってみます。
@OptIn(ExperimentalSerializationApi::class)
abstract class EmptyAsNullSerializer<T : Any>(
private val serializer: KSerializer<T>,
) : KSerializer<T?> {
override val descriptor: SerialDescriptor = SerialDescriptorForEmptyAsNull(serializer.descriptor)
override fun serialize(encoder: Encoder, value: T?) {
if (value != null) {
encoder.encodeNotNullMark()
encoder.encodeSerializableValue(serializer, value)
} else {
encoder.encodeNull()
}
}
override fun deserialize(decoder: Decoder): T? {
val input = decoder as JsonDecoder
val element = input.decodeJsonElement()
if (element is JsonNull) return null
if (element is JsonArray) return null
return input.json.decodeFromJsonElement(serializer, element)
}
}
@OptIn(ExperimentalSerializationApi::class)
internal class SerialDescriptorForEmptyAsNull(
internal val original: SerialDescriptor,
) : SerialDescriptor by original {
override val serialName: String = original.serialName + "[]?"
override val isNullable: Boolean
get() = true
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is SerialDescriptorForEmptyAsNull) return false
if (original != other.original) return false
return true
}
override fun toString(): String = "$original[]?"
override fun hashCode(): Int = original.hashCode() * 31
}
利用する際は、以下のように実際の型のserialiserを渡すだけでSerialiserを作ることができます。
object TitleDataSerializer : EmptyAsNullSerializer<TitleData>(TitleData.serializer())
データクラスでこのSerialiserを指定すればOKです。
@Serializable
data class Data(
@SerialName("data")
@Serializable(with = TitleDataSerializer::class)
val data: TitleData? = null,
) {
@Serializable
data class TitleData(
@SerialName("title")
val title: String,
)
}