#JsonClassDiscriminator doesn't change json class discriminator - kotlin

The Problem
Due to project architecture, backward compatibility and so on, I need to change class discriminator on one abstract class and all classes that inherit from it. Ideally, I want it to be an enum.
I tried to use #JsonClassDiscriminator but Kotlinx still uses type member as discriminator which have name clash with member in class. I changed member name to test what will happen and Kotlinx just used type as discriminator.
Also, outside of annotations, I want to avoid changing these classes. It's shared code, so any non backward compatible changes will be problematic.
Code
I prepared some code, detached from project, that I use for testing behavior.
fun main() {
val derived = Derived("type", "name") as Base
val json = Json {
prettyPrint = true
encodeDefaults = true
serializersModule = serializers
}.encodeToString(derived)
print(json)
}
#Serializable
#JsonClassDiscriminator("source")
data class Derived(
val type: String?,
val name: String?,
) : Base() {
override val source = FooEnum.A
}
#Serializable
#JsonClassDiscriminator("source")
abstract class Base {
abstract val source: FooEnum
}
enum class FooEnum { A, B }
internal val serializers = SerializersModule {
polymorphic(Base::class) {
subclass(Derived::class)
}
}
If I don't change type member name, I got this error:
Exception in thread "main" java.lang.IllegalArgumentException:
Polymorphic serializer for class my.pack.Derived has property 'type'
that conflicts with JSON class discriminator. You can either change
class discriminator in JsonConfiguration, rename property with
#SerialName annotation or fall back to array polymorphism
If I do change the name, I got this JSON which clearly shows, that json type discriminator wasn't changed.
{
"type": "my.pack.Derived",
"typeChanged": "type",
"name": "name",
"source": "A"
}

Kotlinx Serialization doesn't allow for significant customisation of the default type discriminator - you can only change the name of the field.
Encoding default fields
Before I jump into the solutions, I want to point out that in these examples using #EncodeDefault or Json { encodeDefaults = true } is required, otherwise Kotlinx Serialization won't encode your val source.
#Serializable
data class Derived(
val type: String?,
val name: String?,
) : Base() {
#EncodeDefault
override val source = FooEnum.A
}
Changing the discriminator field
You can use #JsonClassDiscriminator to define the name of the discriminator
(Note that you only need #JsonClassDiscriminator on the parent Base class, not both)
However, #JsonClassDiscriminator is more like an 'alternate name', not an override. To override it, you can set classDiscriminator in the Json { } builder
val mapper = Json {
prettyPrint = true
encodeDefaults = true
serializersModule = serializers
classDiscriminator = "source"
}
Discriminator value
You can change the value of type for subclasses though - use #SerialName("...") on your subclasses.
#Serializable
#SerialName("A")
data class Derived(
val type: String?,
val name: String?,
) : Base()
Including the discriminator in a class
You also can't include the discriminator in your class - https://github.com/Kotlin/kotlinx.serialization/issues/1664
So there are 3 options.
Closed polymorphism
Change your code to use closed polymorphism
Since Base is a sealed class, instead of an enum, you can use type-checks on any Base instance
fun main() {
val derived = Derived("type", "name")
val mapper = Json {
prettyPrint = true
encodeDefaults = true
classDiscriminator = "source"
}
val json = mapper.encodeToString(Base.serializer(), derived)
println(json)
val entity = mapper.decodeFromString(Base.serializer(), json)
when (entity) {
is Derived -> println(entity)
}
}
#Serializable
#SerialName("A")
data class Derived(
val type: String?,
val name: String?,
) : Base()
#Serializable
sealed class Base
Since Base is now sealed, it's basically the same as an enum, so there's no need for your FooEnum.
val entity = mapper.decodeFromString(Base.serializer(), json)
when (entity) {
is Derived -> println(entity)
// no need for an 'else'
}
However, you still need Json { classDiscriminator= "source" }...
Content-based deserializer
Use a content-based deserializer.
This would mean you wouldn't need to make Base a sealed class, and you could manually define a default if the discriminator is unknown.
object BaseSerializer : JsonContentPolymorphicSerializer<Base>(Base::class) {
override fun selectDeserializer(element: JsonElement) = when {
"source" in element.jsonObject -> {
val sourceContent = element.jsonObject["source"]?.jsonPrimitive?.contentOrNull
when (
val sourceEnum = FooEnum.values().firstOrNull { it.name == sourceContent }
) {
FooEnum.A -> Derived.serializer()
FooEnum.B -> error("no serializer for $sourceEnum")
else -> error("'source' is null")
}
}
else -> error("no 'source' in JSON")
}
}
This is a good fit in some situations, especially when you don't have a lot of control over the source code. However, I think this is pretty hacky, and it would be easy to make a mistake in selecting the serializer.
Custom serializer
Alternatively you can write a custom serializer.
The end result isn't that different to the content-based deserializer. It's still complicated, and is still easy to make mistakes with. For these reasons, I won't give a complete example.
This is beneficial because it provides more flexibility if you need to encode/decode with non-JSON formats.
#Serializable(with = BaseSerializer::class)
#JsonClassDiscriminator("source")
sealed class Base {
abstract val source: FooEnum
}
object BaseSerializer : KSerializer<Base> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Base") {
// We have to write our own custom descriptor, because setting a custom serializer
// stops the plugin from generating one
}
override fun deserialize(decoder: Decoder): Base {
require(decoder is JsonDecoder) {"Base can only be deserialized as JSON"}
val sourceValue = decoder.decodeJsonElement().jsonObject["source"]?.jsonPrimitive?.contentOrNull
// same logic as the JsonContentPolymorphicSerializer...
}
override fun serialize(encoder: Encoder, value: Base) {
require(encoder is JsonEncoder) {"Base can only be serialized into JSON"}
when (value) {
is Derived -> encoder.encodeSerializableValue(Derived.serializer(), value)
}
}
}

Related

Kotlinx Serialization, inlining sealed class/interface [duplicate]

This question already has an answer here:
kotlinx deserialization: different types && scalar && arrays
(1 answer)
Closed 7 months ago.
With a structure similar to the following:
#Serializable
sealed class Parameters
#Serializable
data class StringContainer(val value: String): Parameters()
#Serializable
data class IntContainer(val value: Int): Parameters()
#Serializable
data class MapContainer(val value: Map<String, Parameters>): Parameters()
// more such as list, bool and other fairly (in the context) straight forward types
And the following container class:
#Serializable
data class PluginConfiguration(
// other value
val parameters: Parameters.MapContainer,
)
I want to reach a (de)serialization where the paramters are configured as a flexible json (or other) map, as one would usually expect:
{
"parameters": {
"key1": "String value",
"key2": 12,
"key3": {}
}
}
And so on. Effectively creating a flexible structure that is still structured enough to not be completely uncontrolled as Any would be. There's a fairly clearly defined (de)serialization, but I cannot figure how to do this.
I've tried reading the following
https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serialization-guide.md
And I do have a hunch that a polymorphic serializer is needed, but so far I'm bumping in to either generic structures, which I believe is way overkill for my purpose or that it for some reason cannot find the serializer for my subclasses, when writing a custom serializer for Parameters.
Update
So using custom serializers combined with surrogate classes, most things are working. The current problem is when values are put into the map, I get a kotlin.IllegalStateException: Primitives cannot be serialized polymorphically with 'type' parameter. You can use 'JsonBuilder.useArrayPolymorphism' instead. Even when I enable array polymorphism this error arises
The answer with kotlinx deserialization: different types && scalar && arrays is basically the answer, and the one I will accept. However, for future use, the complete code to my solution is as follows:
Class hierarchy
#kotlinx.serialization.Serializable(with = ParametersSerializer::class)
sealed interface Parameters
#kotlinx.serialization.Serializable(with = IntContainerSerializer::class)
data class IntContainer(
val value: Int
) : Parameters
#kotlinx.serialization.Serializable(with = StringContainerSerializer::class)
data class StringContainer(
val value: String
) : Parameters
#kotlinx.serialization.Serializable(with = MapContainerSerializer::class)
data class MapContainer(
val value: Map<String, Parameters>
) : Parameters
#kotlinx.serialization.Serializable
data class PluginConfiguration(
val plugin: String,
val parameters: MenuRunnerTest.MapContainer
)
Serializers:
abstract class BaseParametersSerializer<T : Parameters> : KSerializer<T> {
override val descriptor: SerialDescriptor = JsonElement.serializer().descriptor
override fun serialize(encoder: Encoder, value: T) {
fun toJsonElement(value: Parameters): JsonElement = when (value) {
is IntContainer -> JsonPrimitive(value.value)
is MapContainer -> JsonObject(
value.value.mapValues { toJsonElement(it.value) }
)
is StringContainer -> JsonPrimitive(value.value)
}
val sur = toJsonElement(value)
encoder.encodeSerializableValue(JsonElement.serializer(), sur)
}
override fun deserialize(decoder: Decoder): T {
with(decoder as JsonDecoder) {
val jsonElement = decodeJsonElement()
return deserializeJson(jsonElement)
}
}
abstract fun deserializeJson(jsonElement: JsonElement): T
}
object ParametersSerializer : BaseParametersSerializer<Parameters>() {
override fun deserializeJson(jsonElement: JsonElement): Parameters {
return when(jsonElement) {
is JsonPrimitive -> when {
jsonElement.isString -> StringContainerSerializer.deserializeJson(jsonElement)
else -> IntContainerSerializer.deserializeJson(jsonElement)
}
is JsonObject -> MapContainerSerializer.deserializeJson(jsonElement)
else -> throw IllegalArgumentException("Only ints, strings and strings are allowed here")
}
}
}
object StringContainerSerializer : BaseParametersSerializer<StringContainer>() {
override fun deserializeJson(jsonElement: JsonElement): StringContainer {
return when(jsonElement) {
is JsonPrimitive -> StringContainer(jsonElement.content)
else -> throw IllegalArgumentException("Only strings are allowed here")
}
}
}
object IntContainerSerializer : BaseParametersSerializer<IntContainer>() {
override fun deserializeJson(jsonElement: JsonElement): IntContainer {
return when (jsonElement) {
is JsonPrimitive -> IntContainer(jsonElement.int)
else -> throw IllegalArgumentException("Only ints are allowed here")
}
}
}
object MapContainerSerializer : BaseParametersSerializer<MapContainer>() {
override fun deserializeJson(jsonElement: JsonElement): MapContainer {
return when (jsonElement) {
is JsonObject -> MapContainer(jsonElement.mapValues { ParametersSerializer.deserializeJson(it.value) })
else -> throw IllegalArgumentException("Only maps are allowed here")
}
}
}
This structure should be expandable for lists, doubles and other structures, not included in the example :)

How to combine polymorphic and transforming serializers for nested JSON, using Kotlin serialization?

This is a simplified example of the JSON I want to work with:
{
"useless_info": "useless info",
"data": {
"useless_info2": "useless info 2",
"children": [
{
"kind": "Car",
"data": {
"id": 1,
"transmission": "manual"
}
},
{
"kind": "Boat",
"data": {
"id": 2,
"isDocked": true
}
}
]
}
}
children is an array of vehicle objects. vehicle can be boat or car.
My Problem
The information that I want is nested quite deep (the real JSON is much deeply nested). A hack solution is to model the JSON exactly by writing dozens of nested data classes that references each other. I do not want to do this.
My problem is that while I know how to use JsonTransformingSerializer to unwrap arrays of a single type, and JsonContentPolymorphicSerializer to work with objects of various types, in this situation I believe I require both, but I can't get it to work.
What I Did
Assuming a single type
I tried understanding how it would work if it was a single type.
If the objects I wanted where all of the same type, it would be trivial to implement a JsonTransformingSerializer to cut right into the data I want. In this example, I will assume I only care about the ID, so I can just create a generic Vehicle model.
#Serializable
data class VehicleResponse(
#Serializable(with = VehicleResponseSerializer::class)
#SerialName("data")
val vehicles: List<Vehicle>
)
#Serializable
data class Vehicle(val id: Int)
object VehicleResponseSerializer : JsonTransformingSerializer<List<Vehicle>>(ListSerializer(Vehicle.serializer())) {
override fun transformDeserialize(element: JsonElement): JsonElement {
val vehicles = mutableListOf<JsonElement>()
// equals: [{"kind":"Car","data":{"id":1,"transmission":"manual"}},{"kind":"Boat","data":{"id":2,"isDocked":true}}]
val vehicleArray = element.jsonObject["children"]!!.jsonArray
vehicleArray.forEach { vehicle ->
// equals: {"id":1,"transmission":"manual"}}
val vehicleData = vehicle.jsonObject["data"]!!
vehicles.add(vehicleData)
}
return JsonArray(vehicles.toList())
}
}
The code works perfectly. Calling it out from main, printing the result gives me:
VehicleResponse(vehicles=[Vehicle(id=1), Vehicle(id=2)])
But they are actually Polymorphic!
Assuming one type does not work. I need to work with Car and Boat, and call their respective functions and properties.
I tried to model the structure like this:
#Serializable
data class VehicleResponse(
#Serializable(with = VehicleResponseSerializer::class)
#SerialName("data")
val vehicles: List<Vehicle>
)
#Serializable
abstract class Vehicle {
abstract val id: Int
}
#Serializable
data class Car(
override val id: Int,
val transmission: String,
) : Vehicle()
#Serializable
data class Boat(
override val id: Int,
val isDocked: Boolean,
) : Vehicle()
What I Want
I want to receive a JSON from a server, and instantly be able to deserialize it into a list of Vehicle objects, like the one VehicleResponse has.
I want to navigate through a deeply nested JSON, and unwrap an array that contains various Vehicle objects. For this, I assume I need to use JsonTransformingSerializer.
I want to use polymorphic deserialization to convert each of Vehicle into its corresponding subtype.
The actual ACTUAL problem
The thing that is truly throwing me in a loop is that a polymorphic serializer just does not seem to fit. It's called first before I get to parse the JSON. How am I supposed to decide which serializer to use?
Here's a test implementation:
object VehiclePolymorphicSerializer: JsonContentPolymorphicSerializer<VehicleResponse>(VehicleResponse::class) {
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<out VehicleResponse> {
println("\nselectDeserializer()\n" +
"base element:\n" +
"$element\n")
// this return is a temporary hack, I just want to see the base element by printing it to the console
return VehicleResponse.serializer()
}
}
It prints:
selectDeserializer()
base element:
{"useless_info":"useless info","data":{"useless_info2":"useless info 2","children":[{"kind":"Car","data":{"id":1,"transmission":"manual"}},{"kind":"Boat","data":{"id":2,"isDocked":true}}]}}
That's the whole initial JSON! How am I supposed to decide which deserialization strategy to use, if both Car and Boat are in there? The JsonTransformingSerializer is called after the JsonContentPolymorphicSerializer.
Really not sure how am I supposed to proceed here. Would really appreciate even a slight hint.
kotlinx.serialization can handle polymorphic deserialization in this case without custom JsonContentPolymorphicSerializer. You just need to preserve class descriminator in the JSON returned from your JsonTransformingSerializer:
object VehicleResponseSerializer : JsonTransformingSerializer<List<Vehicle>>(ListSerializer(Vehicle.serializer())) {
override fun transformDeserialize(element: JsonElement): JsonElement {
// equals: [{"kind":"Car","data":{"id":1,"totalWheels":"4"}},{"kind":"Boat","data":{"id":2,"isDocked":true}}]
val vehicleArray = element.jsonObject["children"]!!.jsonArray
// equals: [{"type":"Car","id":1,"totalWheels":"4"}, {"type":"Boat","id":2,"isDocked":true}]
return JsonArray(vehicleArray.map {
val data = it.jsonObject["data"]!!.jsonObject
val type = it.jsonObject["kind"]!!
JsonObject(
data.toMutableMap().apply { this["type"] = type }
/*
//Kotlin 1.4 offers a nicer way to do this:
buildMap {
putAll(data)
put("type", type)
}
*/
)
})
}
}
If you declare Vehicle class as sealed (not just abstract), you're already good to go. If you want to keep it abstract, then you need to register all its subclasses in serializersModule
val module = SerializersModule {
polymorphic(Vehicle::class) {
subclass(Car::class)
subclass(Boat::class)
}
}
and pass it to JSON configuration
val kotlinx = Json {
ignoreUnknownKeys = true
serializersModule = module
}
UPDATE
(Alternative approach with combination of JsonTransformingSerializer & JsonContentPolymorphicSerializer)
Actually, it's possible to combine these two serializers.
For the sake of justification for this approach, let's imagine that original JSON doesn't have that nice kind field, and we have to figure out the actual subtype of Vehicle by the shape of JSON. In this case it could be the following heuristic: "if there is a isDocked field, then it's a Boat, othrewise - a Car".
Yes, we may include this logic into JsonTransformingSerializer to create class descriminator on the fly:
val type = when {
"isDocked" in data -> JsonPrimitive("Boat")
else -> JsonPrimitive("Car")
}
But it's more common (and type-safe) to use JsonContentPolymorphicSerializer for this. So we may simplify JsonTransformingSerializer:
object VehicleResponseSerializer : JsonTransformingSerializer<List<Vehicle>>(ListSerializer(Vehicle.serializer())) {
override fun transformDeserialize(element: JsonElement): JsonElement {
// equals: [{"kind":"Car","data":{"id":1,"totalWheels":"4"}},{"kind":"Boat","data":{"id":2,"isDocked":true}}]
val vehicleArray = element.jsonObject["children"]!!.jsonArray
// equals: [{"id":1,"totalWheels":"4"}, {"id":2,"isDocked":true}] // Note that class discriminator is absent here!
return JsonArray(vehicleArray.map { it.jsonObject["data"]!! })
}
}
and define JsonContentPolymorphicSerializer (for Vehicle, not for VehicleResponse!) to handle actual serializer selection:
object VehiclePolymorphicSerializer : JsonContentPolymorphicSerializer<Vehicle>(Vehicle::class) {
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<out Vehicle> = when {
"isDocked" in element.jsonObject -> Boat.serializer()
else -> Car.serializer()
}
}
Since JsonTransformingSerializer (aka VehicleResponseSerializer) is registered for vehicles field serialization it's called before JsonContentPolymorphicSerializer (aka VehiclePolymorphicSerializer). Actually, the latter one is not yet called at all. We need to explicitly register it in serializersModule and pass it to JSON configuration (regardless of whether Vehicle class is declared as abstract or sealed):
val module = SerializersModule {
polymorphicDefault(Vehicle::class) { VehiclePolymorphicSerializer }
}
val kotlinx = Json {
ignoreUnknownKeys = true
serializersModule = module
}

How do i serialize a generic sealed class with kotlinx.serialization

Not sure if it is possible yet but for the life of me I cannot figure out how to serialize this.
sealed class ServiceResult<out T : Any> {
data class Success<out T : Any>(val data: T) : ServiceResult<T>()
data class Error(val exception: Exception) : ServiceResult<Nothing>()
}
Everything that is stuff into T is using #Serializable ex:
#Serializable
data class GalleryDTO(
override val id: Int,
override val dateCreated: Long,
override val dateUpdated: Long,
val name:String,
val description:String,
val photos:List<DTOMin>
) : DTO
As Animesh Sahu already mentioned there is an issue for this topic that is still open, but the solution using a surrogate suggested by Михаил Нафталь for serialization of Error can actually be used also to serialize the polymorphic ServiceResult, by creating a surrogate that mixes the fields of Success and Error. For the sake of simplicity in the example I only represent the exception message.
#Serializable(with = ServiceResultSerializer::class)
sealed class ServiceResult<out T : Any> {
data class Success<out T : Any>(val data: T) : ServiceResult<T>()
data class Error(val exceptionMessage: String?) : ServiceResult<Nothing>()
}
class ServiceResultSerializer<T : Any>(
tSerializer: KSerializer<T>
) : KSerializer<ServiceResult<T>> {
#Serializable
#SerialName("ServiceResult")
data class ServiceResultSurrogate<T : Any>(
val type: Type,
// The annotation is not necessary, but it avoids serializing "data = null"
// for "Error" results.
#EncodeDefault(EncodeDefault.Mode.NEVER)
val data: T? = null,
#EncodeDefault(EncodeDefault.Mode.NEVER)
val exceptionMessage: String? = null
) {
enum class Type { SUCCESS, ERROR }
}
private val surrogateSerializer = ServiceResultSurrogate.serializer(tSerializer)
override val descriptor: SerialDescriptor = surrogateSerializer.descriptor
override fun deserialize(decoder: Decoder): ServiceResult<T> {
val surrogate = surrogateSerializer.deserialize(decoder)
return when (surrogate.type) {
ServiceResultSurrogate.Type.SUCCESS ->
if (surrogate.data != null)
ServiceResult.Success(surrogate.data)
else
throw SerializationException("Missing data for successful result")
ServiceResultSurrogate.Type.ERROR ->
ServiceResult.Error(surrogate.exceptionMessage)
}
}
override fun serialize(encoder: Encoder, value: ServiceResult<T>) {
val surrogate = when (value) {
is ServiceResult.Error -> ServiceResultSurrogate(
ServiceResultSurrogate.Type.ERROR,
exceptionMessage = value.exceptionMessage
)
is ServiceResult.Success -> ServiceResultSurrogate(
ServiceResultSurrogate.Type.SUCCESS,
data = value.data
)
}
surrogateSerializer.serialize(encoder, surrogate)
}
}
This solution can also be easily extended to support nullable Ts. In this case when deserializing you will also have to check if null is a valid value for T (it can be done by checking descriptor.isNullable on tSerializer) and you will also have to cast data as T.
Polymorphic serialization will be a mess in this case (you will have to manually register all possible types passed as a generic parameter to ServiceResult<T>), and will have several limitations (it would be impossible to register primitive types (including Nothing and String) as generic parameters, for instance).
If you only need serialization (aka encoding), I'd recommend to serialize both subtypes independently (for convenience, wrap subtype determination into auxilary function):
inline fun <reified T : Any> serializeServiceResult(x: ServiceResult<T>) = when (x) {
is ServiceResult.Success -> Json.encodeToString(x)
is ServiceResult.Error -> Json.encodeToString(x)
}
To serialize ServiceResult.Success you need just to mark it with #Serializable annotation. The tricky part here is serialization of ServiceResult.Error, or more precisely, serialization of its exception: Exception field. I'd suggest to serialize only its message (via surrogate):
sealed class ServiceResult<out T : Any> {
#Serializable
data class Success<out T : Any>(val data: T) : ServiceResult<T>()
#Serializable(with = ErrorSerializer::class)
data class Error(val exception: Exception) : ServiceResult<Nothing>()
}
#Serializable
private data class ErrorSurrogate(val error: String)
class ErrorSerializer : KSerializer<ServiceResult.Error> {
override val descriptor: SerialDescriptor = ErrorSurrogate.serializer().descriptor
override fun deserialize(decoder: Decoder): ServiceResult.Error {
val surrogate = decoder.decodeSerializableValue(ErrorSurrogate.serializer())
return ServiceResult.Error(Exception(surrogate.error))
}
override fun serialize(encoder: Encoder, value: ServiceResult.Error) {
val surrogate = ErrorSurrogate(value.exception.toString())
encoder.encodeSerializableValue(ErrorSurrogate.serializer(), surrogate)
}
}

Kotlinx Serializer and single-value domain types

I love to use single-value types for e.g. IDs. How can I tell Kotlinx Serializer to consider such types as regular fields, not objects?
Example:
data class User(
val id: UserId,
val name: String,
)
should be serialized to:
{
id: 123,
name: "foo"
}
and UserId is just data class(val value: Int)?
EDIT: I guess I could use custom serializer for each Id class, but I really don't wanna repeat such code for each and every ID in my domain. Moreover, I am not sure how to write one; so far my effort is failing.
EDIT2: this is my attempt for writing such serializer:
open class DoctorIdSerializer : KSerializer<DoctorId> {
override val descriptor: SerialDescriptor = PrimitiveDescriptor("id", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: DoctorId) {
encoder.encodeInt(value.value)
}
override fun deserialize(decoder: Decoder): DoctorId {
return DoctorId(decoder.decodeInt())
}
}
basically, whats written here but its not working :(

Custom serializer with polymorphic kotlinx serialization

With kotlinx.serialization polymorphism, I want to get
{"type":"veh_t","owner":"Ivan","bodyType":"cistern","carryingCapacityInTons":5,"detachable":false}
but I get
{"type":"kotlin.collections.LinkedHashMap","owner":"Ivan","bodyType":"cistern","carryingCapacityInTons":5,"detachable":false}
I use the following models
interface Vehicle {
val owner: String
}
#Serializable
#SerialName("veh_p")
data class PassengerCar(
override val owner: String,
val numberOfSeats: Int
) : Vehicle
#Serializable
#SerialName("veh_t")
data class Truck(
override val owner: String,
val body: Body
) : Vehicle {
#Serializable
data class Body(
val bodyType: String,
val carryingCapacityInTons: Int,
val detachable: Boolean
//a lot of other fields
)
}
I apply the following Json
inline val VehicleJson: Json get() = Json(context = SerializersModule {
polymorphic(Vehicle::class) {
PassengerCar::class with PassengerCar.serializer()
Truck::class with TruckKSerializer
}
})
I use serializer TruckKSerializer because the server adopts a flat structure. At the same time, in the application I want to use an object Truck.Body. For flatten I override fun serialize(encoder: Encoder, obj : T) and fun deserialize(decoder: Decoder): T in Serializator using JsonOutput and JsonInput according to the documentation in these classes.
object TruckKSerializer : KSerializer<Truck> {
override val descriptor: SerialDescriptor = SerialClassDescImpl("Truck")
override fun serialize(encoder: Encoder, obj: Truck) {
val output = encoder as? JsonOutput ?: throw SerializationException("This class can be saved only by Json")
output.encodeJson(json {
obj::owner.name to obj.owner
encoder.json.toJson(Truck.Body.serializer(), obj.body)
.jsonObject.content
.forEach { (name, value) ->
name to value
}
})
}
#ImplicitReflectionSerializer
override fun deserialize(decoder: Decoder): Truck {
val input = decoder as? JsonInput
?: throw SerializationException("This class can be loaded only by Json")
val tree = input.decodeJson() as? JsonObject
?: throw SerializationException("Expected JsonObject")
return Truck(
tree.getPrimitive("owner").content,
VehicleJson.fromJson<Truck.Body>(tree)
)
}
}
And finally, I use stringify(serializer: SerializationStrategy<T>, obj: T)
VehicleJson.stringify(
PolymorphicSerializer(Vehicle::class),
Truck(
owner = "Ivan",
body = Truck.Body(
bodyType = "cistern",
carryingCapacityInTons = 5,
detachable = false
)
)
)
I end up with {"type":"kotlin.collections.LinkedHashMap", ...}, but I need {"type":"veh_t", ...}
How do I get the right type? I want using polymorphism for Vehicle and encode Body object with Truck.Body.serializer() to flatten.
With this serialization, the PassengerCar class runs fine.
VehicleJson.stringify(
PolymorphicSerializer(Vehicle::class),
PassengerCar(
owner = "Oleg",
numberOfSeats = 4
)
)
Result is correct:
{"type":"veh_p","owner":"Oleg","numberOfSeats":4}
I think the problem is the custom serializer TruckKSerializer.
And I noticed if I use in my overridden fun serialize(encoder: Encoder, obj : T) next code
encoder
.beginStructure(descriptor)
.apply {
//...
}
.endStructure(descriptor)
I get the correct type but cannot flatten the object Truck.Body using its serializer.
the correct way to open and close a composite {}
is this code
val composite = encoder.beginStructure(descriptor)
// use composite instead of encoder here
composite.endStructure(descriptor)
and you should be able to serialize Body using .encodeSerializable(Body.serializer(), body)
and always pass the descriptor along otherwise it will fall back to stuff like that LinkedhashMap for the json dictionary