Using a KClass reference as a reified parameter to deserialize from JSON - json

I'm trying to implement a general serialization framework to convert outgoing and incoming messages to json using the kotlinx serialialization. I'm developing a multiplatform app, so I'm trying to get it to run on KotlinJVM and KotlinJS.
For this, I add a type field to every message and use a map that maps each type string to a KClass. What's the type for that map? It contains KClass<> objects whose classes extend the Message class, therefore in java I'd specify my map as
Map<KClass<? extends Message>, String>.
How can I do that in Kotlin?
Afterwards I need to serialize and deserialize the message based on its key and therefore type. Java frameworks take a Class parameter for the type of the object I want to deserialize/instantiate (e.g. gson.fromJson(ClientMessage.class)). In Kotlin this is done using reified parameters Json.decodeFromString<Type>. I do not know the type of the message at compile time though and just have a reference to a KClass, how can I instantiate an object based on that?
#Serializable
open class Message(val type: String) {
companion object {
val messageTypes: Map<KClass<out Message>, String> = mapOf(
ClientLoginMessage::class to "clientLoginMessage",
Message::class to "message"
)
inline fun <reified T> getMessageTypeByClass(): String = messageTypes[T::class]!! // utility for defining the type in the constructors of the individual messages
}
fun toJson() = Json.encodeToString(this)
fun fromJson(json: String): Message? {
val plainMessage = Json.decodeFromString<Message>(json) // get type string from json
return messageTypes.entries.find { it.value == plainMessage.type }?.let {
// how can I use the KClass from it.key as reified parameter?
Json.decodeFromString<?????>(json)
}
}
}
#Serializable
class ClientLoginMessage
: Message(Message.getMessageTypeByClass<ClientLoginMessage>()) {}

Create a map of serializers like for types:
val serializers: Map<KClass<out Message>, KSerializer<out Message>> = mapOf(
ClientLoginMessage::class to ClientLoginMessage.serializer(),
Message::class to Message.serializer()
)
Pass in the serializer needed to Json.decodeFromString like this:
fun fromJson(json: String): Message? {
val plainMessage = Json.decodeFromString<Message>(json) // get type string from json
return messageTypes.entries.find { it.value == plainMessage.type }?.let {
// how can I use the KClass from it.key as reified parameter?
Json.decodeFromString(serializers.get(plainMessage.type)!!, json)
}
}
You might also want to have a look at the Kotlin built in handling of polymorphic classes: https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md

Related

How to serialize a Kotlin Set to a JSON object whose values are empty objects?

I would like to make a Kotlin class serializable using Kotlin serialization.
The class is very simple, something like this:
#Serializable(with = CustomSerializer::class)
data class MyObject(val keys: Set<String>)
Now, I need the serialization format to be a JSON object where the keys are given by the Set<String> and the values are always empty JSON objects.
Example:
val example = MyObject(setOf("abc", "def"))
Should serialize to:
{ "abc": {}, "def": {} }
The reason is that this object is being sent to an API where that's how they want the JSON to look like... the empty objects could contain some directives but I don't want or need to use those.
Having trouble doing that by just reading the documentation.
I've found one way to do it... and it seems simple enough!
I realized that I can get a serializer of empty Objects almost for free with this:
#Serializable
private object EmptyMap
Now, I can write a custom serializer in a straightforward way:
object MyObjectSerializer : KSerializer<MyObject> {
private val _delegate = MapSerializer(String.serializer(), EmptyMap.serializer())
override val descriptor: SerialDescriptor = _delegate.descriptor
override fun serialize(encoder: Encoder, value: MyObject) {
val data = value.keys.associateWith { EmptyMap }
encoder.encodeSerializableValue(_delegate, data)
}
override fun deserialize(decoder: Decoder): MyObject {
val value = decoder.decodeSerializableValue(_delegate)
return MyObject(value.keys)
}
}
Now all that's left to do is to apply the serializer on the type, which can be done with:
#Serializer(with = MyObjectSerializer)
data class MyObject(val keys: Set<String>)
Running Json.encodeToString(example) on the examples works perfectly.

Serializing Java Path with kotlinx.serialization

With kotlinx.serialization, this code will throw an error:
println(Json.encodeToString(Path.of("value")))
saying kotlinx.serialization.SerializationException: Class 'WindowsPath' is not registered for polymorphic serialization in the scope of 'Path'.
WindowsPath is internal, therefore I can not register it as a polymorphic subclass (as in this example), only with Path itself, and even a custom KSerializer for Path throws the same exact error.
Is there any way to have Path properly serialize/deserialize without having to store it as a string?
Path is an interface, so it's implicitly serializable with the PolymorphicSerializer strategy. This strategy requires you to register serializers for subclasses implementing it, but as you know, in this case it's impossible.
There is a default polymorphic serializer, but it affects only deserialization process and it works only when deserializable value is a JSONObject.
And for the following serializer
object PathAsStringSerializer : KSerializer<Path> {
override val descriptor = PrimitiveSerialDescriptor("Path", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: Path) = encoder.encodeString(value.toAbsolutePath().toString())
override fun deserialize(decoder: Decoder): Path = Path.of(decoder.decodeString())
}
\\Not working
val module = SerializersModule { polymorphicDefault(Path::class) { PathAsStringSerializer } }
val decoded : Path = Json { serializersModule = module }.decodeFromString("C:\\Temp")
it will throw runtime exception kotlinx.serialization.json.internal.JsonDecodingException: Expected class kotlinx.serialization.json.JsonObject as the serialized body of kotlinx.serialization.Polymorphic<Path>, but had class kotlinx.serialization.json.JsonLiteral
So, it can't be serialized in a common way, and there are 3 cases of its serialization/deserialization, which need to be handled:
1. Serialization of simple Path variable
In this case you need to explicitly pass your custom serializer:
val path = Path.of("C:\\Temp")
val message1 = Json.encodeToString(PathAsStringSerializer, path).also { println(it) }
println(Json.decodeFromString(PathAsStringSerializer, message1))
2. Serialization of classes, which use Path as a generic parameter
In this case you need to define separate serializers (you may reference original PathAsStringSerializer) and also explicitly pass them:
object ListOfPathsAsStringSerializer : KSerializer<List<Path>> by ListSerializer(PathAsStringSerializer)
val message2 = Json.encodeToString(ListOfPathsAsStringSerializer, listOf(path)).also { println(it) }
println(Json.decodeFromString(ListOfPathsAsStringSerializer, message2))
#Serializable
data class Box<T>(val item: T)
object BoxOfPathSerializer : KSerializer<Box<Path>> by Box.serializer(PathAsStringSerializer)
val message3 = Json.encodeToString(BoxOfPathSerializer, Box(path)).also { println(it) }
println(Json.decodeFromString(BoxOfPathSerializer, message3))
3. Serialization of classes, which have fields of aforementioned types
In this case you need to add respectful #Serializable(with = ...) annotations for these fields:
#Serializable
data class InnerObject(
#Serializable(with = ListOfPathsAsStringSerializer::class)
val list: MutableList<Path> = mutableListOf(),
#Serializable(with = PathAsStringSerializer::class)
val path: Path,
#Serializable(with = BoxOfPathSerializer::class)
val box: Box<Path>
)
Or just list them once for a whole file:
#file: UseSerializers(PathAsStringSerializer::class, ListOfPathsAsStringSerializer::class, BoxOfPathSerializer::class)
Plugin-generated serializer for this case would be good enough:
val message4 = Json.encodeToString(InnerObject(mutableListOf(path), path, Box(path))).also { println(it) }
println(Json.decodeFromString<InnerObject>(message4))

Include class name (Kotlin data class) in JSON response in Quarkus

I'm new to Quarkus and Kotlin and truth be told, I'm not quite sure yet what goes on behind the scenes and which JSON library is actually responsible for rendering the JSON response from a resource when I set the #Produces(MediaType.APPLICATION_JSON) on my function. But I'm returning an instance of a data class that I created from that method and all of the fields in that data class are rendered in the response. However, I have multiple response classes and I would like to include the name of the class in the JSON response. What I have now is a String field that is simply hard coded to the name of the class but that is ugly as I have to repeat the class name:
data class StuffInitiatedResponse (
val id: String,
val projectId: String
) {
val operation = "StuffInitiatedResponse"
}
data class StuffCompletedResponse (
val id: String,
val projectId: String,
) {
val operation = "StuffCompletedResponse"
}
And in my service class:
#Path("/myservice")
class MyService {
#POST
#Path("{project}/{path:.*}")
#Produces(MediaType.APPLICATION_JSON)
fun initiateStuff(#PathParam project: String,
#PathParam path: String,
#QueryParam("completedId") completedId: String?) : StuffInitiatedResponse {
if (completedId == null) {
println("I've initiated stuff")
return StuffInitiatedResponse(UUID.randomUUID().toString(), project)
} else {
println("I've completed stuff")
return StuffCompletedResponse(completedId, project)
}
}
}
This produces what I expect but as I said, I'm annoyed that I have to repeat the class name in the "response" field of the data classes. Is there some way for me to have the class name embedded in the JSON?
The JSON library depends on the dependencies you defined. It can be either Jackson or Yasson.
I recommend using Jackson and, in this case, you can use the #JsonTypeInfo annotation on your serialized classes, which has some options to include the type in the JSON output.

How do I use Moshi to serialize a json string into org.json.JSONObject?

I have a JSON response from my server which is dynamic in nature and I cannot map it into a Kotlin Data Class.
I would like to create a org.json.JSONObject out of it and parse it from there.
I've looked around SO and Moshi's doc but couldn't find any easy way of achieving this.
Any suggestions?
I've stumbled upon the same problem recently. I needed to resend some of the data from one endpoint to another with adding some new stuff to it.
The response from the server looks like this:
{
"someProperty": "value",
"someOtherProperty": "otherValue",
"someDynamicProperty": {
// There may be anything including nested structures, not known beforehand
}
}
Normally Moshi doesn't have built-in support for something like this, but it allows you to build your own adapters and handle the parsing logic.
What you need is define the type that you want to receive as a result:
#JsonClass(generateAdapter = true)
data class CustomResponse(
val someProperty: String,
val someOtherProperty: String,
val someDynamicProperty: JSONObject?
)
Then, you need to create a custom adapter to handle the parsing:
internal object JSONObjectAdapter {
#FromJson
fun fromJson(reader: JsonReader): JSONObject? {
// Here we're expecting the JSON object, it is processed as Map<String, Any> by Moshi
return (reader.readJsonValue() as? Map<String, Any>)?.let { data ->
try {
JSONObject(data)
} catch (e: JSONException) {
// Handle error if arises
}
}
}
#ToJson
fun toJson(writer: JsonWriter, value: JSONObject?) {
value?.let { writer.value(Buffer().writeUtf8(value.toString())) }
}
}
Then, just add this adapter to Moshi builder on creation:
Moshi.Builder().add(JSONObjectAdapter).build()
or use Moshi annotations if you want to apply it only to some particular properties.

How to model finite set of values of enum-like type for (de)serialization?

I am using Spray-json 1.3.1. I have the following JSON message:
{
"results": [{
... NOT IMPORTANT PART HERE ...
}],
"status": "OK"
}
Trivially, this can be deserialized to status String field via
case class Message[T](results: List[T], status: String)
with custom Protocol
object MessageProtocol extends DefaultJsonProtocol {
implicit def messageFormat[T: JsonFormat] = jsonFormat2(Message.apply[T])
}
Since status field can be one of OK, ZERO_RESULTS, OVER_QUERY_LIMIT having this field as a String makes no sense. As I am coming from
Java background I tried enums in Scala implemented as follows:
case class Message[T](results: List[T], status: Status)
object Status extends Enumeration{
type Status = Value
val OK,ZERO_RESULTS,OVER_QUERY_LIMIT, REQUEST_DENIED, INVALID_REQUEST,UNKNOWN_ERROR = Value
}
object MessageProtocol extends DefaultJsonProtocol {
implicit val statusFormat = jsonFormat(Status)
implicit def messageFormat[T: JsonFormat] = jsonFormat2(Message.apply[T])
}
What is best practice/approach to solve this?
You can simply implement your own RootJsonFormat (as an implicit in Message companion object) and override read and write functions. There you will have JsObject and you can convert it to your own case class as you want like converting the string to desired enumeration etc. You can see a sample here