Сериализация Kotlin универсального типа, который можно сериализовать

#json #kotlin #generics #annotations #kotlinx.serialization

Вопрос:

Сериализация Котлина-это сложно! Как мне заставить Котлина поверить, что значения на моей карте свойств являются либо примитивами, либо классами с аннотациями @Serializable ?

Я пытаюсь превратить такой класс: class Entity(val id: String, val type: String, val properties: Map<String, *>) где я знаю, что * это примитивно или @Serializable или String в JSON, например: { id: "0", type: "falcon", max-flight-range-nm: 10 }

 import kotlinx.serialization.*
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.encodeToJsonElement

fun main() {
    val json = Json {
        prettyPrint = true
    }
    val falcon = Entity("0", "falcon", mapOf("max-flight-range-nm" to 10))
    println(json.encodeToString(falcon))
    // Desired JSON (no nesting of 'Entity.parameters', they bump-up into parent intentionally
    // { id: "0", type: "falcon", max-flight-range-nm: 10 }
    // Avoiding extra nesting is conceptually similar to Jackson's @JsonAnyGetter annotation: https://www.baeldung.com/jackson-annotations#1-jsonanygetter
}

@Serializable
class Entity(val id: String, val type: String, @Contextual val properties: Map<String, *>) {

    @Serializer(forClass = Entity::class)
    companion object : KSerializer<Entity> {
        override fun serialize(encoder: Encoder, value: Entity) {
            encoder.encodeString(value.id)
            encoder.encodeString(value.type)
            // attempted hack to encode properties at this level
            for ((mapKey, mapValue) in value.properties) {
                val valueJson = Json.encodeToJsonElement(mapValue)
                val jsonObject = JsonObject(mapOf(mapKey to valueJson))
                encoder.encodeString(jsonObject.toString())
            }
        }
    }
}
 

У меня есть полный контроль над всем кодом, поэтому при необходимости я мог бы зайти так далеко, чтобы написать кучу sealed class PropertyValue подклассов и пометить их как @Serializable — так IntPropertyValue , StringPropertyValue , FooPropertyValue , и т. Д. Может быть, это единственный выход?

К сожалению, вышеперечисленное не удается во время компиляции (хотя в Intelli-J ошибок нет).: org.jetbrains.kotlin.codegen.CompilationException: Back-end (JVM) Internal error: Serializer for element of type Any? has not been found.

Комментарии:

1. Если это невозможно, я вернусь к объектному изображению GSON или Джексона.

2. Проблема не в том, чтобы заставить сериализацию kotlinx во что-то верить; она «верит» в то, что вы пытаетесь сделать. Он говорит вам, правильно, он не знает, каких типов ожидать или как их различать. Вам нужна полиморфная сериализация, которая довольно хорошо документирована. Подумайте о том, чтобы написать ручной синтаксический анализатор JSON: как бы вы узнали, в каком виде сериализовать значения на карте? Что библиотека просит вас сделать, так это определить механизм. Использование полиморфной сериализации с помощью дискриминатора классов является распространенным подходом, но другие описаны документально.

Ответ №1:

У меня есть полный контроль над всем кодом, поэтому при необходимости я мог бы зайти так далеко, чтобы написать кучу закрытых подклассов класса PropertyValue и пометить их как @Serializable — так что IntPropertyValue, StringPropertyValue, FooPropertyValue и т. Д. Может быть, это единственный выход?

Вам нужно сделать PropertyValue это самому @Serializable (см. https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md) и, конечно, используйте val properties: Map<String, PropertyValue> вместо val properties: Map<String, *> (так как в сообщении об ошибке говорится «Сериализатор для элемента типа Any?» возможно, вы забыли этот шаг).

Также обратите внимание, что по умолчанию вы не получите

 { id: "0", type: "falcon", max-flight-range-nm: 10 }
 

но, например (опуская ключевые кавычки)

 { id: "0", type: "falcon", properties: { 
    max-flight-range-nm: { 
        type: "your_package.IntPropertyValue", value: 10 
    } 
} }
 

Если вы хотите проанализировать/создать этот конкретный JSON, напишите пользовательский сериализатор, и тогда вам тоже может сойти с Map<String, *> рук (но это все равно обычно плохая идея).