I'm trying to deserialize JSON into different sealed subclasses, the class mappings work, but the actual values are all null
Example1:
{
"eventName": "SUCCESS",
"productName": "BLAH"
}
Example2:
{
"eventName": "FAILURE",
"productName": "BLAH",
"error": "Something went wrong"
}
The base sealed class looks like this:
#ExperimentalSerializationApi
#Serializable(with = EventSerializer::class)
sealed class Event {
val eventName: String? = null
val productName: String? = null
}
I have three subclasses
#Serializable
class EventFailure : Event()
#Serializable
class EventSuccess : Event()
#Serializable
class EventInvalid : Event()
and this is the Serializer
#ExperimentalSerializationApi
#Serializer(forClass = Event::class)
object EventSerializer : JsonContentPolymorphicSerializer<Event>(Event::class) {
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<out Event> {
return element.jsonObject["eventName"]?.jsonPrimitive?.content?.let {
when (EventType.valueOf(it)) {
EventType.FAILURE -> EventFailure.serializer()
EventType.SUCCESS -> EventSuccess.serializer()
}
} ?: EventInvalid.serializer()
}
}
When I deserialize a JSON list, all the values end up null:
val events = JSON.decodeFromString<Array<Event>>(it.body())
events.forEach {
println(it)
println(it.productName)
}
com.blah.EventSuccess#2ca132ad
null
com.blah.EventFailure#1686f0b4
null
If I change Event from a sealed class to a normal class without the custom serializer, data correctly deserializes into the Even class filling in all the values.
Any suggestions on how to make the deserialization into EventSuccess / EventFailure work correctly?
I made a custom JSON class with custom classDiscriminator
companion object {
val JSON = Json {
prettyPrint = true
ignoreUnknownKeys = true
encodeDefaults = true
classDiscriminator = "eventName"
}
}
#Serializable
#SerialName("SUCCESS")
class EventSuccess : Event()
#Serializable
#SerialName("FAILURE")
class EventFailure : Event()
and I removed the custom serializer
//#Serializable(with = EventSerializer::class)
#Serializable
sealed class Event {
and it's working correctly now.
I'm keeping the question open to see if there are perhaps ways of fixing the custom serializer implementation.
Related
I have several classes that I want to deserialize, that include lists of polymorphic types.
I can get it to deserialize correctly known types, but deserializing an unknown type throws an exception. What I really want is that the list includes only known types and unknown types are just filtered out.
If this could be done in a generic way would be even better.
sealed interface BaseInterface
interface I1 : BaseInterface {
fun f1(): String
}
interface I2: BaseInterface {
fun f2(): String
}
#Serializable
#SerialName("i1")
data class I1Rest(value: String): I1 {
fun f1(): String = value
}
#Serializable
#SerialName("i2")
data class I2Rest(value: String): I2 {
fun f2(): String = value
}
#Serializable
data class SomeClass(list: List<BaseInterface>)
How can I can correctly deserialize SomeClass with
{ "list": [ {"type": "i1", "value": "v1" }, {"type": "i2", "value": "v2" }, {"type": "i3", "value": "v3" } ] }
If I don't add the i3 type to the json, I can correctly deserialize it using
SerializersModule {
polymorphic(BaseInterface::class){
subclass(I1Rest::class)
subclass(I2Rest::class)
}
}
But as soon as I include an unknown type, it breaks. Note that I don't want to deserialize unknowns to a default type (that would have to extend the base sealed interface). What I want is to ignore/filter the unknowns. (preferably in a generic way)
I would also would like to keep the BaseInterface as an interface and not a class, because I only want to expose interfaces and not concrete classes (this is for a lib)
Ok, this is the best I could come up with:
#Serializable
data class SomeClass(
#Serializable(with = UnknownBaseInterfaceTypeFilteringListSerializer::class)
val list: List<BaseInterface>
)
val json = Json {
ignoreUnknownKeys = true
serializersModule = SerializersModule {
polymorphic(BaseInterface::class) {
subclass(I1Rest::class)
subclass(I2Rest::class)
defaultDeserializer { UnknownTypeSerializer() }
}
}
}
class FilteringListSerializer<E>(private val elementSerializer: KSerializer<E>) : KSerializer<List<E>> {
private val listSerializer = ListSerializer(elementSerializer)
override val descriptor: SerialDescriptor = listSerializer.descriptor
override fun serialize(encoder: Encoder, value: List<E>) {
listSerializer.serialize(encoder, value)
}
override fun deserialize(decoder: Decoder): List<E> = with(decoder as JsonDecoder) {
decodeJsonElement().jsonArray.mapNotNull {
try {
json.decodeFromJsonElement(elementSerializer, it)
} catch (e: UnknownTypeException) {
null
}
}
}
}
class UnknownTypeException(message: String) : SerializationException(message)
open class UnknownTypeSerializer<T> : KSerializer<T> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Nothing")
override fun deserialize(decoder: Decoder): T = throw UnknownTypeException("unknown type")
override fun serialize(encoder: Encoder, value: T) = throw UnknownTypeException("unknown type")
}
object UnknownBaseInterfaceTypeFilteringListSerializer : KSerializer<List<BaseInterface>> by FilteringListSerializer(PolymorphicSerializer(BaseInterface::class))
I want to decode a json string containing a list of objects in a polymorphic class structure using kotlinx.serialization in a Kotlin Multiplatform project, but it works only on JVM, not on Native. Here is a minimum reproducible example:
#Serializable
abstract class Project {
abstract val name: String
}
#Serializable
#SerialName("BasicProject")
data class BasicProject(override val name: String): Project()
#Serializable
#SerialName("OwnedProject")
data class OwnedProject(override val name: String, val owner: String) : Project()
fun main() {
val data = Json.decodeFromString<List<Project>>("""
[
{"type":"BasicProject","name":"example"},
{"type":"OwnedProject","name":"kotlinx.serialization","owner":"kotlin"}
]
"""))
}
This works on JVM but throws the following exception on Native:
kotlinx.serialization.SerializationException: Serializer for class ‘Project’ is not found.
Mark the class as #Serializable or provide the serializer explicitly.
On Kotlin/Native explicitly declared serializer should be used for interfaces and enums without #Serializable annotation.message
This problem has been discussed before in the context of encoding and some workarounds have been suggested, e.g. here, but my problem is decoding. Is there a workaround, or do I simply have to implement my own json parser?
You need to explicitly pass respectful serializer and serializersModule:
object ListOfProjectSerializer : KSerializer<List<Project>> by ListSerializer(Project.serializer())
val module = SerializersModule {
polymorphic(Project::class) {
subclass(BasicProject::class)
subclass(OwnedProject::class)
}
}
fun main() {
val data = Json { serializersModule = module }.decodeFromString(
ListOfProjectSerializer,
"""
[
{"type":"BasicProject","name":"example"},
{"type":"OwnedProject","name":"kotlinx.serialization","owner":"kotlin"}
]
"""
)
}
I have two simple entities:
class EntityA #JsonCreator(mode = JsonCreator.Mode.DELEGATING) constructor(
val entityB: EntityB
) : DomainEvent {
//How to include propertyTwo and propertyThree here?
}
class EntityB #JsonCreator constructor (
#JsonProperty("propertyTwo")
val propertyTwo:String,
#JsonProperty("propertyThree")
val propertyThree:String ;
) {
}
With this mapping I need this result JSON serialization and deserialization:
{
"propertyOne": "",
"propertyTwo": "",
"propertyThree": "",
}
Is there any way to mapping this scenario with Jackson annotations in EntityB class or entityB property? If not, how do I create a custom deserializer to help me with this issue?
Unfortunately its not possible today, use data class with this approach. So was needed comment "propertyOne".
The final solution I used:
class EntityA #JsonCreator(mode = JsonCreator.Mode.DELEGATING) constructor(
//#JsonProperty("propertyOne")
//val propertyOne:String,
#field:JsonUnwrapped val entityB: EntityB
) : DomainEvent {
//How to include propertyTwo and propertyThree here?
}
But apparently there is a error with data class and #JsonUnwrapper :
https://github.com/FasterXML/jackson-module-kotlin/issues/56
I have created a sealed class for the json field Value under CustomAttribute data class. This field can return String or Array of Strings.
How can we deserialize this sealed class from json?
data class CustomAttribute (
val attributeCode: String,
val value: Value
)
sealed class Value {
class StringArrayValue(val value: List<String>) : Value()
class StringValue(val value: String) : Value()
}
One solution is to use a RuntimeTypeAdapterFactory as per the instructions in this answer
val valueTypeAdapter = RuntimeTypeAdapter.of(Value::class.java)
.registerSubtype(StringArrayValue::class.java)
.registerSubtype(StringValue::class.java)
val gson = GsonBuilder().registerTypeAdapter(valueTypeAdapter).create()
RuntimeTypeAdapter is included in the source code for Gson but not exposed as a Maven artifact.
It is designed to be copy/pasted into your project from here
I created a TypeAdapterFactory implementation specifically to support sealed classes and their subtypes. This works similarly to the RuntimeTypeAdapterFactory (and I used it as a guide to write my class), but will specifically only support sealed types, and will deserialize using object instances of objects with a sealed class supertype (RuntimeTypeAdapterFactory will create a new instance of object types, which breaks equality checks when a single instance is the expectation).
private class SealedTypeAdapterFactory<T : Any> private constructor(
private val baseType: KClass<T>,
private val typeFieldName: String
) : TypeAdapterFactory {
private val subclasses = baseType.sealedSubclasses
private val nameToSubclass = subclasses.associateBy { it.simpleName!! }
init {
if (!baseType.isSealed) throw IllegalArgumentException("$baseType is not a sealed class")
}
override fun <R : Any> create(gson: Gson, type: TypeToken<R>?): TypeAdapter<R>? {
if (type == null || subclasses.isEmpty() || subclasses.none { type.rawType.isAssignableFrom(it.java) }) return null
val elementTypeAdapter = gson.getAdapter(JsonElement::class.java)
val subclassToDelegate: Map<KClass<*>, TypeAdapter<*>> = subclasses.associateWith {
gson.getDelegateAdapter(this, TypeToken.get(it.java))
}
return object : TypeAdapter<R>() {
override fun write(writer: JsonWriter, value: R) {
val srcType = value::class
val label = srcType.simpleName!!
#Suppress("UNCHECKED_CAST") val delegate = subclassToDelegate[srcType] as TypeAdapter<R>
val jsonObject = delegate.toJsonTree(value).asJsonObject
if (jsonObject.has(typeFieldName)) {
throw JsonParseException("cannot serialize $label because it already defines a field named $typeFieldName")
}
val clone = JsonObject()
clone.add(typeFieldName, JsonPrimitive(label))
jsonObject.entrySet().forEach {
clone.add(it.key, it.value)
}
elementTypeAdapter.write(writer, clone)
}
override fun read(reader: JsonReader): R {
val element = elementTypeAdapter.read(reader)
val labelElement = element.asJsonObject.remove(typeFieldName) ?: throw JsonParseException(
"cannot deserialize $baseType because it does not define a field named $typeFieldName"
)
val name = labelElement.asString
val subclass = nameToSubclass[name] ?: throw JsonParseException("cannot find $name subclass of $baseType")
#Suppress("UNCHECKED_CAST")
return (subclass.objectInstance as? R) ?: (subclassToDelegate[subclass]!!.fromJsonTree(element) as R)
}
}
}
companion object {
fun <T : Any> of(clz: KClass<T>) = SealedTypeAdapterFactory(clz, "type")
}
}
Usage:
GsonBuilder().registerTypeAdapter(SealedTypeAdapterFactory.of(Value::class)).create()
I have successfully serialized and de-serialized a sealed class in the past, with a disclaimer of using Jackson, not Gson as my serialization engine.
My sealed class has been defined as:
#JsonTypeInfo(use = JsonTypeInfo.Id.MINIMAL_CLASS, include = JsonTypeInfo.As.PROPERTY, visible = true)
sealed class FlexibleResponseModel
class SnapshotResponse(val collection: List<EntityModel>): FlexibleResponseModel()
class DifferentialResponse(val collection: List<EntityModel>): FlexibleResponseModel()
class EventDrivenResponse(val collection: List<EntityEventModel>): FlexibleResponseModel()
class ErrorResponse(val error: String): FlexibleResponseModel()
With the annotations used, it required no further configuration for the Jackson instance to properly serialize and de-serialize instances of this sealed class granted that both sides of the communication possessed a uniform definition of the sealed class.
While I recognise that JsonTypeInfo is a Jackson-specific annotation, perhaps you might consider switching over from Gson if this feature is a must - or you might be able to find an equivalent configuration for Gson which would also include the class identifier in your serialized data.
Let's assume I have following json objects :
{
"type": "video",
"...": "..."
}
{
"type": "image",
"...": "..."
}
They both represent media object. Kotlin sealed model looks like :
sealed class Media {
...
}
#Serializable
#SerialName("video")
data class Video(...) : Media()
#Serializable
#SerialName("image")
data class Image(...) : Media()
According KoltinX doc, I used a wrapper for polymorphic serialization :
#Serializable
private data class MediaWrapper(#Polymorphic val media: Media) {
companion object {
val jsonSerializer = Json(
context = SerializersModule {
polymorphic<Media> {
Video::class with Video.serializer()
Image::class with Image.serializer()
}
}
)
fun fromJson(json: String) = jsonSerializer.parse(serializer(), json)
}
}
The goal is to deserialize a Media json using my wrapper, but problem is I need to change my Media json into a MediaWrapper json.
The most convenient solution I found is to add {\"media\":\" & \"} on each side of my Media json:
sealed class Media {
companion object {
fun fromJson(mediaJson: String): Media {
val mediaWrapperJson = "{\"media\":$mediaJson}"
val mediaWrapper = MediaWrapper.fromJson(mediaWrapperJson)
return mediaWrapper.media
}
}
}
This is a trick, if there is a more convenient way to deserialize polymorphics, please let me know!
While the kotlinx serialization docs use a wrapper in many of its polymorphic examples, it does not say that this pattern is mandatory.
From the docs:
Pro tip: to use Message without a wrapper, you can pass PolymorphicSerializer(Message::class) to parse/stringify.
In your case you could do:
sealed class Media {
companion object {
val jsonSerializer = Json(
context = SerializersModule {
polymorphic<Media> {
Video::class with Video.serializer()
Image::class with Image.serializer()
}
}
)
fun fromJson(mediaJson: String): Media {
return jsonSerializer.parse(PolymorphicSerializer(Media::class), mediaJson) as Media
}
}
}