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.
Related
All the API JSON responses would have the following structure:
{
"status": <Integer>
"data": <Object or List of Objects>
"message": <String>
}
the only property that changes is the 'data', which can be any object or list of object.
So is there a way to create a BaseResponse class like
open class BaseResponse<T> (
#SerializedName("status")
val status: Int,
#SerializedName("data")
abstract val `data`: T,
#SerializedName("message")
val message: String
)
and the response classes
data class HelloResponse (
override val `data`: Hello
) : BaseResponse<Hello> {
data class Hello (
#SerializedName("hello")
val hello: String
)
}
data class HellosResponse (
override val `data`: List<Hello>
) : BaseResponse<List<Hello>> {
data class Hello (
#SerializedName("hello")
val hello: String
)
}
What i really want is to only override the data property, so that i don't have to write status and message property for each Response sub data class i write. I dont want to write status and message in my sub class and pass it to base class, cause i'd still write both the properties, so no difference than creating a data class with status and message.
so cannot be like
data class HelloResponse (
val status: Int,
override val `data`: Hello,
val message: String
) : BasicResponse<Hello>(status, `data`, message) {
data class Hello (
#SerializedMessage("hello")
val hello: String
)
}
Edit: Own Answer
Well I realized that the HelloResponse is actually a waste since i'm only using it to access the actual Hello class.
So what i did was to use the Base class directly in Retrofit2 service.
Eg:
fun hello(): Call<BaseResponse<Hello>>
or
fun hellos(): Call<BaseResponse<List<Hello>>>
Well you have to directly specify the type with BaseResponse everywhere you use it. Maybe create typeallias
Or you can create alias
typealias HelloResponse = BaseResponse<Hello>
typealias HellosResponse = BaseResponse<List<Hello>>
To manually deserialize json string with Gson, you need to use TypeToken parameter instead of class type.
val hello = Gson().fromJson<BaseResponse<Hello>>(jsonStr, object: TypeToken<BaseResponse<Hello>>(){}.type)
If you use
val hello = Gson().fromJson<BaseResponse<Hello>>(jsonStr, BaseResponse::class.java)
The data property doesn't convert to Hello instead converts to LinkedHashMap
Note:
Retrofit2's GsonConverterFactory uses TypeToken internally, so no problem.
If you don't want to write status and message properties for data subclasses then you cannot expect subclass to have a constructor with status and message magically.
I strongly suggest you to make BaseResponse abstract and make subclasses like following
abstract class BaseResponse<T> {
#SerializedName("status")
abstract val status: Int
#SerializedName("message")
abstract val message: String
#SerializedName("data")
abstract val `data`: T
}
data class HelloResponse (
override val status: Int,
override val message: String,
override val `data`: Hello,
) : BaseResponse<Hello>() {
data class Hello (
#SerializedMessage("hello")
val hello: String
)
}
You can achieve it in a way you don't need to write override val properties for subclass declarations by sacrificing data classes. However you lose all bounties provided by data class.
abstract class BaseResponse<T> {
#SerializedName("status")
abstract val status: Int
#SerializedName("message")
abstract val message: String
#SerializedName("data")
abstract val `data`: T
}
class HelloResponse: BaseResponse<Hello>() {
data class Hello (
#SerializedMessage("hello")
val hello: String
)
}
Just a kind reminder, you don't need to use #SerializedName annotation if class property name and json property name are same.
Intro
I'm sending JSON messages between two backend servers that use different languages. The producing
server creates a variety of JSON messages, wrapped inside a message with metadata.
The wrapping class is Message, The consuming server has to determine which type of message its
receiving based solely on the message contents.
When I try to use a star-projection to
deserialize the message, I get an error.
Example
import kotlinx.serialization.json.Json
#Language("JSON")
val carJson = """
{
"message_type": "some message",
"data": {
"info_type": "Car",
"name": "Toyota"
}
}
""".trimIndent()
// normally I wouldn't know what the Json message would be - so the type is Message<*>
val actualCarMessage = Json.decodeFromString<Message<*>>(carJson)
Error message
Exception in thread "main" java.lang.IllegalArgumentException: Star projections in type arguments are not allowed, but Message<*>
at kotlinx.serialization.SerializersKt__SerializersKt.serializerByKTypeImpl$SerializersKt__SerializersKt(Serializers.kt:81)
at kotlinx.serialization.SerializersKt__SerializersKt.serializer(Serializers.kt:59)
at kotlinx.serialization.SerializersKt.serializer(Unknown Source)
at ExampleKt.main(example.kt:96)
at ExampleKt.main(example.kt)
Class structure
I want to deserialize JSON into a data class, Message, that has a field with a generic type.
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
#Serializable
data class Message<out DataType : SpecificInformation>(
#SerialName("message_type")
val type: String,
#SerialName("data")
val data: DataType,
)
The field is constrained by a sealed interface, SpecificInformation, with some implementations.
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonClassDiscriminator
#JsonClassDiscriminator("info_type")
sealed interface SpecificInformation {
#SerialName("info_type")
val infoType: String
}
#Serializable
#SerialName("User")
data class UserInformation(
#SerialName("info_type")
override val infoType: String,
val name: String,
) : SpecificInformation
// there are more implementations...
Workaround?
This is a known
issue (kotlinx.serialization/issues/944)
,
so I'm looking for workarounds.
I have control over the JSON structure and libraries - though I have a preference for
kotlinx.serialization.
I can't change that there are two JSON objects, one is inside the other, and the discriminator is
inside the inner-class.
A custom serializer would be great. But I'd prefer to have this configured on the class or file
(with #Serializable(with = ...) or #file:UseSerializers(...)) as using a
custom SerializersModule is not as seamless.
Attempt: JsonContentPolymorphicSerializer
I've written a custom serializer, which only if it's used specifically (which is something I'd like
to avoid). It's also quite clunky, breaks if the data classes change or a new one is added, and
doesn't benefit from the sealed interface.
Can this be improved so that
It can be used generically? Json.decodeFromString<Message<*>>(carJson)
It doesn't have any hard-coded strings?
class MessageCustomSerializer : JsonContentPolymorphicSerializer<Message<*>>(Message::class) {
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<out Message<*>> {
val discriminator = element
.jsonObject["data"]
?.jsonObject?.get("info_type")
?.jsonPrimitive?.contentOrNull
println("found discriminator $discriminator")
val subclassSerializer = when (discriminator?.lowercase()) {
"user" -> UserInformation.serializer()
"car" -> CarInformation.serializer()
else -> throw IllegalStateException("could not find serializer for $discriminator")
}
println("found subclassSerializer $subclassSerializer")
return Message.serializer(subclassSerializer)
}
}
fun main() {
#Language("JSON")
val carJson = """
{
"message_type": "another message",
"data": {
"info_type": "Car",
"brand": "Toyota"
}
}
""".trimIndent()
val actualCarMessage =
Json.decodeFromString(MessageCustomSerializer(), carJson)
val expectedCarMessage = Message("another message", CarInformation("Car", "Toyota"))
require(actualCarMessage == expectedCarMessage) {
println("car json parsing ❌")
}
println("car json parsing ✅")
}
#Serializable(with = ... - infinite loop
I tried applying MessageCustomSerializer directly to Message...
#Serializable(with = MessageCustomSerializer::class)
data class Message<out T : SpecificInformation>(
//...
But then I couldn't access the plugin-generated serializer, and this causes an infinite loop.
return Message.serializer(subclassSerializer) // calls 'MessageCustomSerializer', causes infinite loop
#Serializer(forClass = ...) - not generic
In addition to annotating Message with #Serializable(with = MessageCustomSerializer::class), I
tried
deriving a plugin-generated serializer:
#Serializer(forClass = Message::class)
object MessagePluginGeneratedSerializer : KSerializer<Message<*>>
But this serializer is not generic, and causes an error
java.lang.AssertionError: No such value argument slot in IrConstructorCallImpl: 0 (total=0).
Symbol: MessageCustomSerializer.<init>|-5645683436151566731[0]
at org.jetbrains.kotlin.ir.expressions.IrMemberAccessExpressionKt.throwNoSuchArgumentSlotException(IrMemberAccessExpression.kt:66)
at org.jetbrains.kotlin.ir.expressions.IrFunctionAccessExpression.putValueArgument(IrFunctionAccessExpression.kt:31)
at org.jetbrains.kotlinx.serialization.compiler.backend.ir.IrBuilderExtension$DefaultImpls.irInvoke(GeneratorHelpers.kt:210)
at org.jetbrains.kotlinx.serialization.compiler.backend.ir.SerializableCompanionIrGenerator.irInvoke(SerializableCompanionIrGenerator.kt:35)
You are asking many things here, so I will simply try to give some pointers in regards to the errors you are making which you seem to be stuck on. With those in mind, and reading the documentation I link to, I believe you should be able to resolve the rest yourself.
Polymorphic serialization
Acquaint yourself with kotlinx.serialization polymorphic serialization. When you are trying to serialize Message<*> and DataType you are trying to use polymorphic serialization.
In case you are serializing Message<*> as the root object, specifying PolymorphicSerializer explicitly (as I also posted in the bug report you link to) should work. E.g., Json.decodeFromString( PolymorphicSerializer( Message::class ), carJson ).
P.s. I'm not 100% certain what you are trying to do here is the same as in the bug report. Either way, specifying the serializer explicitely should work, whether or not it is a bug that you shouldn't be required to do so.
The message_type and info_type fields you have in Message and DataType respectively are class discriminators. You need to configure this in your Json settings, and set the correct SerialName on your concrete classes for them to work. Using a different class discriminator per hierarchy is only possible starting from kotlinx.serialization 1.3.0 using #JsonClassDiscriminator.
Overriding plugin-generated serializer
But then I couldn't access the plugin-generated serializer, and this causes an infinite loop.
#Serializable(with = ...) overrides the plugin-generated serializer. If you want to retain the plugin-generated serializer, do not apply with.
When you are serializing the object directly (as the root object), you can still pass a different serializer to use as the first parameter to encode/decode. When you want to override the serializer to use for a specific property nested somewhere in the root object, use #Serializable on the property.
Polymorphism and generic classes
The "No such value argument slot in IrConstructorCallImpl: 0" error is to be expected.
You need to do more work in case you want to specify a serializer for polymorphic generic classes.
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
I have a controller handling a route like 'POST /doit', the json body is automatically parsed into a case class using Finatra built in tools (Jackson, etc), something like this:
class MyController extends Controller {
post("/doit") { request: MyRequest =>
// something
}
}
case class MyRequest(
id: String,
custom: String
)
Here are some valid requests:
{ "id": "my id", "custom": "my custom" }
{ "id": "my id", "custom": "{'x': 'y'}" }
As you can see, 'custom' field can be a JSON which can't be deserialized because Jackson expect it to be a POJO instead of a String, I tried wrapping this JSON with quotes but they are ignored and the field is handled as JSON.
How can I let Jackson library to know that this field should be kept plain?
I had read and the best solution I came up with is to write a custom deserializer, in this case, I have not idea how to integrate with Finatra.
As "Ryan O'Neill" pointed out in Finatra Google Group, there are examples for writing a custom deserializer in ExampleCaseClasses.scala.
I'm copying the following code from previous scala source:
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
case class CaseClassWithCustomDecimalFormat(
#JsonDeserialize(using = classOf[MyBigDecimalDeserializer])
myBigDecimal: BigDecimal,
#JsonDeserialize(using = classOf[MyBigDecimalDeserializer])
optMyBigDecimal: Option[BigDecimal])
class MyBigDecimalDeserializer extends JsonDeserializer[BigDecimal] {
override def deserialize(jp: JsonParser, ctxt: DeserializationContext): BigDecimal = {
val jsonNode: ValueNode = jp.getCodec.readTree(jp)
BigDecimal(jsonNode.asText).setScale(2, RoundingMode.HALF_UP)
}
override def getEmptyValue: BigDecimal = BigDecimal(0)
}
Thanks Ryan.
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