How to parse JSON objects into an enum - kotlin

I have a JSON object that is something like
{
"tsp": "ABC" // can be only one of three things: "ABC", "DEF", "GHI"
"userId" : "lkajsdlk-199191-lkjdflakj"
}
Instead of writing a dataclass along the lines of
data class User(#SerializedName("tsp") val tsp: String, #SerializedName("userId") val userId: String
i'd like to have an enum that defines the three values so that my data class can be
data class User(#SerializedName("tsp") val tsp: TspEnum, #SerializedName("userId") val userId: String
I had tried writing an enum that was
enum class TspEnum(provider: String) {
AY_BEE_CEE("ABC"),
DEE_EE_EFF("DEF"),
GEE_HAYTCH_I("GHI");
}
however that did not work out
I've realized now that calling TspEnum.provider will return the value of the enum, but I'm not sure how to make GSON coƶperate whilst serializing the JSON into the kotlin data class
I've read that there is an issue with Kotlin typing and GSON here: https://discuss.kotlinlang.org/t/json-enum-deserialization-breakes-kotlin-null-safety/11670
however, the way that person is serializing the hair colours to map into an enum is different enough from my tsp json object to make me scratch my head.
Any pointers on where i'm going wrong would be great, cheers!

You can create a deserializer for TspEnum:
class TspDeserializer : JsonDeserializer<TspEnum> {
override fun deserialize(json: JsonElement, typeOfT: Type?, context: JsonDeserializationContext?): TspEnum {
val stringValue = json.asString
for (enum in TspEnum.values()) {
if (enum.provider == stringValue) {
return enum
}
}
throw IllegalArgumentException("Unknown tsp $stringValue!")
}
}
next you have to register it:
val gson = GsonBuilder()
.registerTypeAdapter(TspEnum::class.java, TspDeserializer())
.create()
and then you can parse your user:
val user = gson.fromJson(json, User::class.java)
println(user) // prints User(tsp=AY_BEE_CEE, userId=lkajsdlk-199191-lkjdflakj)

Related

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
}

Serialize enum field into JSON in Kotlin

I've got a stupid question that stunned me a bit.
I have an enum and a data class like this:
enum class MyEventType(val typeName: String) {
FIRST("firstEventReceived")
}
data class MyEvent(
val id: String,
val event: MyEventType
)
I need to send this as a json string, but common desearilizer makes such a json
{
"id": "identifier",
"event": "FIRST"
}
but i need
{
"id": "identifier",
"event": "firstEventReceived"
}
As far as i understand, kotlin allows to override getter in data class, but i didn't succeed in it... Trying to make
data class MyEvent(
val id: String
) {
val event: MyEventType get() event.typeName
}
but i've missed something, i guess...
The simplest way is probably to annotate the property with #JsonValue:
enum class MyEventType(#JsonValue val typeName: String) {
FIRST("firstEventReceived")
}
data class MyEvent(
val id: String,
val event: MyEventType
)
fun main() {
MyEvent(id = "foo", event = MyEventType.FIRST)
.let { jacksonObjectMapper().writeValueAsString(it) }
.let { println(it) }
}
Prints:
{"id":"foo","event":"firstEventReceived"}
The easiest way is to annotate the typeName with #JsonValue. This will serialise and deserialise the enum field as you want.
enum class MyEventType(#JsonValue val typeName: String) {
FIRST("firstEventReceived");
}
An alternative is to use #JsonFormat (if you are using jackson version < 2.9);
enum class MyEventType(#JsonFormat(shape = JsonFormat.Shape.OBJECT) val typeName: String) {
FIRST("firstEventReceived");
}
Herer's an example;
#JvmStatic
fun main(args: Array<String>) {
val mapper = jacksonObjectMapper()
val json = mapper.writeValueAsString(MyEvent("1", MyEventType.FIRST))
println(json)
val event = mapper.readValue<MyEvent>(json)
println(event)
}
You get the output;
{"id":"1","event":"firstEventReceived"}
MyEvent(id=1, event=FIRST)
I used Jackson version 2.12.0. Here's a good read on enum manipulation with Jackson - https://www.baeldung.com/jackson-serialize-enums
Also you can have enum with 2+ fields which you want to be serialized
enum class MyEventType(
val firstField: String,
val secondField: String,
val thirdField: String
) {
MY_ENUM("firstFieldValue", "secondFieldValue", "thirdFieldValue")
}
You can chose one of the following two options:
Put #JsonValue over a method(lets call it getter) that will return the required value(if you need only part of the fields):
#JsonValue
fun getSerializedObject(): String {
return "{firstField: $firstField, thirdField: $thirdField}"
}
Result will be "{firstField: firstFieldValue, thirdField: thirdFieldValue}"
Put #JsonFormat(shape = JsonFormat.Shape.OBJECT) over your enum class(for serialization class as common class):
#JsonFormat(shape = JsonFormat.Shape.OBJECT)
enum class MyEventType(
val firstField: String,
val secondField: String,
val thirdField: String
) {
MY_ENUM("firstField", "secondField", "thirdField")
}
Result will be "{"firstField": "firstFieldValue", "secondField": "secondFieldValue", "thirdField": "thirdFieldValue"}"
For GSON users, you can use the #SerializedName annotation:
enum class ConnectionStatus {
#SerializedName("open")
OPEN,
#SerializedName("connecting")
CONNECTING,
#SerializedName("closed")
CLOSED
}

Map Key Values to Dataclass in Kotlin

how can I set properties of a dataclass by its name. For example, I have a raw HTTP GET response
propA=valueA
propB=valueB
and a data class in Kotlin
data class Test(var propA: String = "", var propB: String = ""){}
in my code i have an function that splits the response to a key value array
val test: Test = Test()
rawResp?.split('\n')?.forEach { item: String ->
run {
val keyValue = item.split('=')
TODO
}
}
In JavaScript I can do the following
response.split('\n').forEach(item => {
let keyValue = item.split('=');
this.test[keyValue[0]] = keyValue[1];
});
Is there a similar way in Kotlin?
You cannot readily do this in Kotlin the same way you would in JavaScript (unless you are prepared to handle reflection yourself), but there is a possibility of using a Kotlin feature called Delegated Properties (particularly, a use case Storing Properties in a Map of that feature).
Here is an example specific to code in your original question:
class Test(private val map: Map<String, String>) {
val propA: String by map
val propB: String by map
override fun toString() = "${javaClass.simpleName}(propA=$propA,propB=$propB)"
}
fun main() {
val rawResp: String? = """
propA=valueA
propB=valueB
""".trimIndent()
val props = rawResp?.split('\n')?.map { item ->
val (key, value) = item.split('=')
key to value
}?.toMap() ?: emptyMap()
val test = Test(props)
println("Property 'propA' of test is: ${test.propA}")
println("Or using toString: $test")
}
This outputs:
Property 'propA' of test is: valueA
Or using toString: Test(propA=valueA,propB=valueB)
Unfortunately, you cannot use data classes with property delegation the way you would expect, so you have to 'pay the price' and define the overridden methods (toString, equals, hashCode) on your own if you need them.
By the question, it was not clear for me if each line represents a Test instance or not. So
If not.
fun parse(rawResp: String): Test = rawResp.split("\n").flatMap { it.split("=") }.let { Test(it[0], it[1]) }
If yes.
fun parse(rawResp: String): List<Test> = rawResp.split("\n").map { it.split("=") }.map { Test(it[0], it[1]) }
For null safe alternative you can use nullableString.orEmpty()...

Type conversion of declarative client URI argument

I am implementing a declarative client in Micronaut that looks like this:
#Get("/dostuff{?requestObject*}")
fun getStuff(requestObject: MyRequestObject): String
My MyRequestObject contains an enum that is represented by some string:
data class MyRequestObject(val myEnum: MyEnum)
enum class MyEnum(val stringRep: String) {
AREASONABLENAME("someSillyString");
}
When I now send a request via the client the value from requestObject generates the following query /?myEnum=AREASONABLENAME. What I actually need is /?myEnum=someSillyString.
I tried the following things without any success:
add JsonValue function to MyEnum:
#JsonValue fun getJsonValue() = stringRep - of course did not help
implement a TypeConverter for MyEnum
#Singleton
class MyEnumTypeConverter : TypeConverter<MyEnum, String> {
override fun convert(`object`: MyEnum?, targetType: Class<String>?, context: ConversionContext?): Optional<String> {
return Optional.ofNullable(`object`?.stringRep)
}
}
Is there a way to achieve the desired behaviour?
You can override the toString method in the Enum so that when the converter tries to convert it to a string you can control the result of the operation:
enum class MyEnum(val stringRep: String) {
AREASONABLENAME("someSillyString");
override fun toString(): String {
return stringRep
}
}

How can my Kotlin API classes be constructed from json? (using Moshi)

I am refactoring and adding to the API communication of an app. I'd like to get to this usage for my "json data objects". Instantiate with either the properties directly or from a json string.
userFromParams = User("user#example.com", "otherproperty")
userFromString = User.fromJson(someJsonString)!!
// userIWantFromString = User(someJsonString)
Getting userFromParams to serialize to JSON was not a problem. Just adding a toJson() function takes care of that.
data class User(email: String, other_property: String) {
fun toJson(): String {
return Moshi.Builder().build()
.adapter(User::class.java)
.toJson(this)
}
companion object {
fun fromJson(json: String): User? {
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
return moshi.adapter(User::class.java).fromJson(json)
}
}
}
It is "fromJson" that I would like to get rid of ...because... I want to and I can't figure out how. The above class works (give or take wether to allow an optional object to be returned or not and so on) but it just bugs me that I get stuck trying to get to this nice clean overloaded initialization.
It does not strictly have to be a data class either, but it does seem appropriate here.
You can't really do that in any performant way. Any constructor invocation will instantiate a new object, but since Moshi handles object creation internally, you'll have two instances...
If you really REALLY want it though, you can try something like:
class User {
val email: String
val other_property: String
constructor(email: String, other_property: String) {
this.email = email
this.other_property = other_property
}
constructor(json: String) {
val delegate = Moshi.Builder().build().adapter(User::class.java).fromJson(json)
this.email = delegate.email
this.other_property = delegate.other_property
}
fun toJson(): String {
return Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
.adapter(User::class.java)
.toJson(this)
}
}