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
}
}
}
Related
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.
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 have my code structure like this:
File 1:
abstract class SomeClass {
abstract fun print()
companion object {
val versions = arrayOf(ClassV1::class, ClassV2::class)
}
}
#Serializable
data class ClassV1(val x: Int) : SomeClass() {
override fun print() {
println("Hello")
}
}
#Serializable
data class ClassV2(val y: String) : SomeClass() {
override fun print() {
println("World")
}
}
File 2:
fun <T : SomeClass> getSomeObject(json: String, kClass: KClass<T>): SomeClass {
return Json.decodeFromString(json)
}
fun printData(version: Int, json: String) {
val someClass: SomeClass = getSomeObject(json, SomeClass.versions[version])
someClass.print()
}
I have a json in printData that is a serialized form of some sub-class of SomeClass. I also have a version which is used to determine which class structure does the json represent. Based on the version, I want to de-serialize my json string to the appropriate sub-class of SomeClass.
Right now the getSomeObject function deserializes the json to SomeClass (which crashes, as expected). I want to know if there is a way I can deserialize it to the provided KClass.
I know I can do this like below:
val someClass = when (version) {
0 -> Json.decodeFromString<ClassV1>(json)
else -> Json.decodeFromString<ClassV2>(json)
}
But I am trying to avoid this since I can have a lot of such versions. Is there a better way possible?
It seems to me that the following is what you are looking for:
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "version",
visible = false)
#JsonSubTypes(
JsonSubTypes.Type(value = ClassV1::class, name = "V1"),
JsonSubTypes.Type(value = ClassV2::class, name = "V2"))
abstract class SomeClass {
(...)
}
This basically means that your JSON would be deserialized as ClassV1 or ClassV2 based on the JSON property version:
V1 would mean that ClassV1 is the target class;
V2 would mean that ClassV2 is the target class.
You can find more information about this at the following online resources:
https://fasterxml.github.io/jackson-annotations/javadoc/2.4/com/fasterxml/jackson/annotation/JsonTypeInfo.html
https://fasterxml.github.io/jackson-annotations/javadoc/2.5/com/fasterxml/jackson/annotation/JsonSubTypes.Type.html
https://www.baeldung.com/jackson-annotations#jackson-polymorphic-type-handling-annotations
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"}
]
"""
)
}
How can we use kotlin.serialize with Ktor's HttpClient to deserialize/serialize JSON with lists as root? I am creating the HttpClient as follows:
HttpClient {
install(JsonFeature) {
serializer = KotlinxSerializer().apply {
setMapper(MyClass::class, MyClass.serializer())
setMapper(AnotherClass::class, AnotherClass.serializer())
}
}
install(ExpectSuccess)
}
Appears I need to setMapper for List, however that is not possible with generics. I see I can get the serializer for it with MyClass.serializer().list, but registering it to deserialize/serialize on http requests is not straight forward. Anyone know of a good solution?
You can write wrapper and custom serializer:
#Serializable
class MyClassList(
val items: List<MyClass>
) {
#Serializer(MyClassList::class)
companion object : KSerializer<MyClassList> {
override val descriptor = StringDescriptor.withName("MyClassList")
override fun serialize(output: Encoder, obj: MyClassList) {
MyClass.serializer().list.serialize(output, obj.items)
}
override fun deserialize(input: Decoder): MyClassList {
return MyClassList(MyClass.serializer().list.deserialize(input))
}
}
}
Register it:
HttpClient {
install(JsonFeature) {
serializer = KotlinxSerializer().apply {
setMapper(MyClassList::class, MyClassList.serializer())
}
}
}
And use:
suspend fun fetchItems(): List<MyClass> {
return client.get<MyClassList>(URL).items
}
Update with ktor 1.3.0:
Now you're able to receive default collections(such a list) from the client directly:
#Serializable
data class User(val id: Int)
val response: List<User> = client.get(...)
// or client.get<List<User>>(...)
Before ktor 1.3.0:
There is no way to (de)serialize such JSON in the kotlinx.serialization yet.
For serialization you could try something like this:
fun serializer(data: Any) = if (data is List<*>) {
if (data is EmptyList) String::class.serializer().list // any class with serializer
else data.first()::class.serializer().list
} else data.serializer()
And there are no known ways to get the list deserializer.
This is more of a workaround but after stepping through KotlinxSerializer code I couldn't see any other way round it. If you look at KotlinxSerializer.read() for example you can see it tries to look up a mapper based on type but in this case it's just a kotlin.collections.List and doesn't resolve. I had tried calling something like setListMapper(MyClass::class, MyClass.serializer()) but this only works for serialization (using by lookupSerializerByData method in write)
override suspend fun read(type: TypeInfo, response: HttpResponse): Any {
val mapper = lookupSerializerByType(type.type)
val text = response.readText()
#Suppress("UNCHECKED_CAST")
return json.parse(mapper as KSerializer<Any>, text)
}
So, what I ended up doing was something like (note the serializer().list call)
suspend fun fetchBusStops(): List<BusStop> {
val jsonArrayString = client.get<String> {
url("$baseUrl/stops.json")
}
return JSON.nonstrict.parse(BusStop.serializer().list, jsonArrayString)
}
Not ideal and obviously doesn't make use of JsonFeature.
I happened to have the same problem on Kotlin/JS, and managed to fix it this way:
private val client = HttpClient(Js) {
install(JsonFeature) {
serializer = KotlinxSerializer().apply {
register(User.serializer().list)
}
}
}
...
private suspend fun fetchUsers(): Sequence<User> =
client.get<List<User>> {
url("$baseUrl/users")
}.asSequence()
Hope this helps :)