web-dev-qa-db-fra.com

Comment sérialiser / désérialiser la classe scellée Kotlin?

J'ai une classe scellée suivante:

sealed class ViewModel {

  data class Loaded(val value : String) : ViewModel()
  object Loading : ViewModel()

}

Comment puis-je sérialiser/désérialiser des instances de la classe ViewModel, disons vers/depuis le format JSON?

J'ai essayé d'utiliser la bibliothèque de sérialisation/désérialisation Genson - elle peut gérer les classes de données Kotlin, il est également possible de prendre en charge les types polymorphes (par exemple, en utilisant des métadonnées pour spécifier des types concrets).

Cependant, la bibliothèque échoue sur les types Kotlin object, car ce sont des singletons sans constructeur public. Je suppose que je pourrais écrire un convertisseur Genson personnalisé pour le gérer, mais peut-être qu'il existe un moyen plus simple de le faire?

8

J'ai fini par implémenter un convertisseur personnalisé plus une usine pour le brancher correctement sur Genson.

Il utilise la convention de métadonnées de Genson pour représenter l'objet comme:

{ 
  "@class": "com.example.ViewModel.Loading" 
}

Le convertisseur suppose seClassMetadata jeu d'indicateurs, donc la sérialisation a juste besoin de marquer un objet vide. Pour la désérialisation, il résout le nom de classe à partir des métadonnées, le charge et obtient objectInstance.

object KotlinObjectConverter : Converter<Any> {

override fun serialize(objectData: Any, writer: ObjectWriter, ctx: Context) {
    with(writer) {
        // just empty JSON object, class name will be automatically added as metadata
        beginObject()
        endObject()
    }
}

override fun deserialize(reader: ObjectReader, ctx: Context): Any? =
    Class.forName(reader.nextObjectMetadata().metadata("class"))
        .kotlin.objectInstance
        .also { reader.endObject() }
}

Pour m'assurer que ce convertisseur est appliqué uniquement aux réels objets, je l'enregistre à l'aide d'une fabrique, qui indique à Genson quand l'utiliser et quand revenir à l'implémentation par défaut.

object KotlinConverterFactory : Factory<Converter<Any>> {

    override fun create(type: Type, genson: Genson): Converter<Any>? =
        if (TypeUtil.getRawClass(type).kotlin.objectInstance != null) KotlinObjectConverter
        else null

}

L'usine peut être utilisée pour configurer Genson via le constructeur:

GensonBuilder()
        .withConverterFactory(KotlinConverterFactory)
        .useClassMetadata(true) // required to add metadata during serialization
        // some other properties
        .create()

Le code pourrait probablement être encore plus agréable avec la fonction de convertisseurs chaînés, mais je n'ai pas encore eu le temps de le vérifier.

3

Vous avez probablement raison de créer un sérialiseur personnalisé.

J'ai essayé de sérialiser et de désérialiser votre classe en utilisant la bibliothèque Jackson et Kotlin.

Ce sont les dépendances Maven pour Jackson:

<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.8.8</version>
</dependency>

<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.8.8</version>
</dependency>

Vous pouvez sérialiser la classe scellée en JSON à l'aide de cette bibliothèque sans sérialiseurs personnalisés supplémentaires, mais la désérialisation nécessite un désérialiseur personnalisé.

Voici le code du jouet que j'ai utilisé pour sérialiser et dé-sérialiser votre classe scellée:

import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.module.SimpleModule

sealed class ViewModel {
    data class Loaded(val value: String) : ViewModel()
    object Loading : ViewModel()
}

// Custom serializer
class ViewModelDeserializer : JsonDeserializer<ViewModel>() {
    override fun deserialize(jp: JsonParser?, p1: DeserializationContext?): ViewModel {
        val node: JsonNode? = jp?.getCodec()?.readTree(jp)
        val value = node?.get("value")
        return if (value != null) ViewModel.Loaded(value.asText()) else ViewModel.Loading
    }
}

fun main(args: Array<String>) {
    val m = createCustomMapper()
    val ser1 = m.writeValueAsString(ViewModel.Loading)
    println(ser1)
    val ser2 = m.writeValueAsString(ViewModel.Loaded("test"))
    println(ser2)
    val deserialized1 = m.readValue(ser1, ViewModel::class.Java)
    val deserialized2 = m.readValue(ser2, ViewModel::class.Java)
    println(deserialized1)
    println(deserialized2)
}

// Using mapper with custom serializer
private fun createCustomMapper(): ObjectMapper {
    val m = ObjectMapper()
    val sm = SimpleModule()
    sm.addDeserializer(ViewModel::class.Java, ViewModelDeserializer())
    m.registerModule(sm)
    return m
}

Si vous exécutez ce code, voici la sortie:

{}
{"value":"test"}
ViewModel$Loading@1753acfe
Loaded(value=test)
4
gil.fernandes

J'ai eu un problème similaire récemment (bien que j'utilise Jackson, pas Genson.)

En supposant que j'ai les éléments suivants:

sealed class Parent(val name: String)

object ChildOne : Parent("ValOne")
object ChildTwo : Parent("ValTwo")

Ajout d'une fonction JsonCreator à la classe scellée:

sealed class Parent(val name: String) {

    private companion object {
        @JsonCreator
        @JvmStatic
        fun findBySimpleClassName(simpleName: String): Parent? {
            return Parent::class.sealedSubclasses.first {
                it.simpleName == simpleName
            }.objectInstance
        }
    }
}

Vous pouvez maintenant désérialiser en utilisant ChildOne ou ChildTwo comme key dans votre propriété json.

3
SergioLeone