Use Int as a parameter for Kotlin data class - kotlin

got a bit of a problem with creating a data class. First post, and a bit of a noob - be gentle :)
I'm trying to create a Kotlin data class to handle some responses from an API we use; the response of which looks a bit like this:
"data": {
"devices": {
"600": [
{
"device_id": "[deviceId]", ...
What I'm having trouble with is the "600" bit - I can't find a way to create the data class with this as a parameter. Each time I declare the var/val - it's throwing an error, but doesn't provide any helpful options in the IDE. All the rest are strings, so "devices" becomes "val devices: String" and so on. But in this case the val is an Int, and I don't know how to declare this in the data class.
I want to have the API response re-worked to something more easily defined, but that'll take time. Can anyone tell me how I can pass the Int as the parameter?
This is the data class:
data class SimRetrieveDevicesResponse(
val data: Devices,
val error: String? = null,
)
data class Devices(
val 600: List<DeviceInfo>? = null
)
data class DeviceInfo(
val device_id: String,
val device_type: String,
val network_id: String,
val send_period_sec: Int,
val loss_in_thousand: Int,
val tti_application_id: String,
val cmt_tenant_id: String,
)
Sorry I've called anything the wrong name...

Backtick-ing the val has fixed the error, and helped tremendously.

As stated in the comments, the mapping is more than likely for a map, so having a property called "600", back-ticked or not, is incorrect. As soon as you have another value like "700", you'd have to change your code.
Here's a working solution, based on the assumption from your JSON snippet that devices is a map of a list of device information:
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import io.kotest.assertions.withClue
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import org.junit.jupiter.api.Test
data class SimRetrieveDevicesResponse(
val data: Devices,
val error: String? = null,
)
data class Devices(val devices: Map<String, List<DeviceInfo>>)
data class DeviceInfo(
val device_id: String,
val device_type: String,
val network_id: String,
val send_period_sec: Int,
val loss_in_thousand: Int,
val tti_application_id: String,
val cmt_tenant_id: String,
)
class StackOverFlowTest {
#Test
fun test() {
val data = """
{
"data": {
"devices": {
"600": [
{
"device_id": "device_id",
"device_type": "device_type",
"network_id": "network_id",
"send_period_sec": 2,
"loss_in_thousand": 3,
"tti_application_id": "tti_application_id",
"cmt_tenant_id": "cmt_tenant_id"
}
]
}
}
}
""".trimIndent()
val mapper = jacksonObjectMapper()
val response = mapper.readValue(data, SimRetrieveDevicesResponse::class.java)
val device = response.data.devices["600"]
withClue("Device should be present") {
device.shouldNotBe(null)
}
device!!.first().device_id shouldBe "device_id"
}
}
The assertions here use kotest, which you can add via this in your build.gradle.kts
testImplementation("io.kotest:kotest-assertions-core:$kotestVersion")

Related

Type initialization with jackson?

I'm working on data class extension with polymorphic property. Here's the dataclass:
import com.fasterxml.jackson.annotation.JsonSubTypes
import com.fasterxml.jackson.annotation.JsonTypeInfo
data class CarModelResponse(
val models: List<CarType> = listOf(),
)
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
#JsonSubTypes(
JsonSubTypes.Type(MercedesType::class, name = "mercedes"),
JsonSubTypes.Type(OpelType::class, name = "opel"),
)
abstract class CarType(open val type: String) {
abstract fun getCarFeature(): Any
}
data class MercedesType(
val comfortClass: Int
) : CarType("mercedes") {
override fun getCarFeature(): Int = comfortClass
}
data class OpelType(
val coupon: String
) : CarType("opel") {
override fun getCarFeature(): String = coupon
}
and also i have a test, where i'm trying to get a typed car variable:
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import org.junit.jupiter.api.Test
class CarResponseTest {
#Test
fun getCarFeatures() {
val json: String = """
{
"models": [
{
"type": "mercedes",
"comfortClass": 1
},
{
"type": "opel",
"coupon": "Buy Opel and win a Mercedes! Coupon #1."
}
]
}""".trimIndent()
val response = jacksonObjectMapper().readValue<CarModelResponse>(json)
val comfortClass: Int = (response.models.first() as MercedesType).getCarFeature()
val couppon: String = (response.models.last() as OpelType).getCarFeature()
}
}
Deserialization works fine, but I need to retrieve car's feature without casting with as at last two lines of code, but I'm a little stuck how to do that. Could anyone advice how can I get rid of it?
You can't do it with no casting, because compiler needs to know what type must be assigned, however in this case you can avoid casting to MercedesType or OpelType:
val comfortClass: Int = response.models.first().getCarFeature() as Int
val coupon: String = response.models.last().getCarFeature() as String

Convert String referential datatype to real referential datatypes

I have the following dataclasses:
data class JsonNpc(
val name: String,
val neighbours: JsonPreferences
)
data class JsonPreferences(
val loves: List<String>,
val hates: List<String>
)
I have a list of these, and they reference each other through strings like:
[
JsonNpc(
"first",
JsonPreferences(
listOf("second"),
listOf()
)
),
JsonNpc(
"second",
JsonPreferences(
listOf(),
listOf("first")
)
)
]
note that a likes b does not mean b likes a
I also have the Dataclasses
data class Npc(
val name: String,
val neighbours: NeighbourPreferences,
)
data class NeighbourPreferences(
val loves: List<Npc>,
val hates: List<Npc>
)
And I want to convert the String reference types to the normal reference types.
What I have tried:
recursively creating the npcs (and excluding any that are already in the chain, as that would lead to infinite recursion):
Does not work, as the Npc can not be fully created and the List is immutable (I dont want it to be mutable)
I have managed to find a way to do this. It did not work with Npc as a data class, as I needed a real constructor
fun parseNpcs(map: Map<String, JsonNpc>): Map<String, Npc> {
val resultMap: MutableMap<String, Npc> = mutableMapOf()
for (value in map.values) {
if(resultMap.containsKey(value.name))
continue
Npc(value, map, resultMap)
}
return resultMap
}
class Npc(jsonNpc: JsonNpc, infoList: Map<String, JsonNpc>, resultMap: MutableMap<String, Npc>) {
val name: String
val neighbourPreferences: NeighbourPreferences
init {
this.name = jsonNpc.name
resultMap[name] = this
val lovesNpc = jsonNpc.neighbours.loves.map {
resultMap[it] ?: Npc(infoList[it] ?: error("Missing an Npc"), infoList, resultMap)
}
val hatesNpc = jsonNpc.neighbours.hates.map {
resultMap[it] ?: Npc(infoList[it] ?: error("Missing an Npc"), infoList, resultMap)
}
this.neighbourPreferences = NeighbourPreferences(
lovesNpc, hatesNpc
)
}
}
data class NeighbourPreferences(
val loves: List<Npc>,
val hates: List<Npc>
)
checking in the debugger, the people carry the same references for each Neighbour, so the Guide is always one Npc instance.

kotlinx-serialization class marked #Serializable does not have the .serializer() extension function

I have the following data class
#Serializable
data class Income(val id: String,
val description: String,
val amount: Int,
val Time: Date,
val userId: String)
now when I try to use the .serializer() function it says that the .serializer() is not defined for Income class so my project doesn't compile.
val response = Json.stringify(Income.serializer(), Incomes)
call.respond(HttpStatusCode.OK,response)
I Looked twice on the documentation in the readme.md.
Even watched the announcement video from KotlinConf
Did anyone have the same problem. What am i doing wrong??
Edit:
I tried to just copy paste the samples from the readme.md and had the same problem.
import kotlinx.serialization.*
import kotlinx.serialization.json.*
#Serializable
data class Data(val a: Int, val b: String = "42")
fun main() {
// Json also has .Default configuration which provides more reasonable settings,
// but is subject to change in future versions
val json = Json(JsonConfiguration.Stable)
// serializing objects
val jsonData = json.stringify(Data.serializer(), Data(42))
// serializing lists
val jsonList = json.stringify(Data.serializer().list, listOf(Data(42)))
println(jsonData) // {"a": 42, "b": "42"}
println(jsonList) // [{"a": 42, "b": "42"}]
// parsing data back
val obj = json.parse(Data.serializer(), """{"a":42}""") // b is optional since it has default value
println(obj) // Data(a=42, b="42")
}
This does not compile as well in my code.
I'm currently using Kotlin 1.3..61 and
kotlinx-serialization-runtime 0.14.0
In addition to the kotlinx-serialization-runtime dependency you also need to add the plugin
plugins {
kotlin("multiplatform") // or kotlin("jvm") or any other kotlin plugin
kotlin("plugin.serialization") version "1.4.10"
}
with the same version as Kotlin itself.
I had a similar problem that my Navigation Drawer XML file didn't recognize my serializable class, for that I had to extend the java.io.Serializable myself, bit for kotlin it was fine:
#Serializable
data class Income(val id: String,
val description: String,
val amount: Int,
val Time: Date,
val userId: String) : java.io.Serializable

How to describe event structures and obtain information?

I want to write a DSL in kotlin that describes events in an event store to generate Java code, JSON examples, schema and documentation based on these descriptions. My current approach is:
A data class MyEvent that holds the structure of a event
data class MyEvent(val name: String, val version: Int, val content: Map<String, String>)
so that I can describe events like
val OrderCreated = MyEvent("OrderCreated", 1, mapOf("orderId" to "UUID", "nameOfProduct" to "String(1, 256)", "quantity" to "Integer"))
val OrderCancelled = MyEvent("OrderCancelled", 2, mapOf("orderId" to "UUID", "reason" to "String(100, 1000)"))
val OrderQuestioned = MyEvent("OrderQuestioned", 3, mapOf("orderId" to "UUID", "question" to "String(10, 1000)"))
Whereas "String(1, 256)" means it is of type String with a minimum of 1 and a maximum of 256 characters.
To iterate over all events to generate everything I want, I need to manually add each event to a list / set
fun scanForAllMyEventsInstances(): Set<MyEvent> {
return hashSetOf(OrderCreated, OrderCancelled, OrderQuestioned)
}
This way doesn't feel like the best way.
I don't like to be forced to add new events in scanForAllMyEventsInstances. I only want to describe a new event at one point.
I don't like to create an instance of MyEvent for each event. It's all "static" information.
So my question: How would you do that? I'd like to have some suggestions.
I use SpringBoot at the moment. So don't hesitate to suggest frameworks.
I think enum class is your friend here.
enum class MyEvent(val eventName: String, val version: Int, val content: Map<String, String>) {
OrderCreated("OrderCreated", 1, mapOf("orderId" to "UUID", "nameOfProduct" to "String(1, 256)", "quantity" to "Integer")),
OrderCancelled("OrderCancelled", 2, mapOf("orderId" to "UUID", "reason" to "String(100, 1000)")),
OrderQuestioned("OrderQuestioned", 3, mapOf("orderId" to "UUID", "question" to "String(10, 1000)"))
}
And you can iterate over instances
MyEvent.values().forEach { println(it.eventName) }
With the approach of enum classes, I added an interface so that I can organize plenty of (different) events
interface MyEvent2 {
val eventName: String
val version: Int
val content: Map<String, String>
}
enum class MyOrderEvents : MyEvent2 {
OrderCreated {
override val eventName = "OrderCreated"
override val version = 1
override val content = mapOf("orderId" to "UUID", "nameOfProduct" to "String(1, 256)", "quantity" to "Integer")
},
OrderCancelled {
override val eventName = "OrderCancelled"
override val version = 2
override val content = mapOf("orderId" to "UUID", "reason" to "String(100, 1000)")
},
OrderQuestioned {
override val eventName = "OrderQuestioned"
override val version = 3
override val content = mapOf("orderId" to "UUID", "question" to "String(10, 1000)")
}
}
enum class MyUserEvents : MyEvent2 {
UserCreated {
override val eventName = "UserCreated"
override val version = 1
override val content = mapOf("userId" to "UUID", "name" to "String(1, 256)")
},
UserDeleted {
override val eventName = "UserDeleted"
override val version = 2
override val content = mapOf("userId" to "UUID")
},
UserChanged {
override val eventName = "UserChanged"
override val version = 3
override val content = mapOf("userId" to "UUID", "newName" to "String(1, 256)")
}
}
I still need to add each enum class to a list/set.
fun scanForAllMyEventsInstances(): Set<MyEvent2> {
val allEvents = HashSet<MyEvent2>()
allEvents.addAll(MyOrderEvents.values())
allEvents.addAll(MyUserEvents.values())
return allEvents
}
fun main() {
scanForAllMyEventsInstances().forEach{e -> println(e.eventName)}
}
But only once per class. Each new event within that class is automatically recognized. That's fine for me.
remark: The val name should not be used when using an enum class because in enum classes the value name is already defined. That's why in MyEvent2 it's called eventName. Maybe I even can skip name / eventName because it's 1:1 to the enum name.
I will have a try with this.
PS: It's still not a nice DSL, I think. But optimizing this, I will do in a future task.

Using Moshi with multiple input fields

I have some JSON that looks like this:
{
"name" : "Credit Card",
"code" : "AUD",
"value" : 1000
}
and am using Moshi to unmarshall this into a data structure like:
data class Account(
#Json(name = "name")
val name: String,
#Json(name = "currency")
val currency: String,
#Json(name = "value")
val value: Int
)
Everything works well. However, I really would like to extract the currency and value parameters into a separate Money object. So my model looks more like:
data class Money(
#Json(name = "currency")
val currency: String,
#Json(name = "value")
val value: Int
)
data class Account(
#Json(name = "name")
val name: String,
#Json(name = "???")
val money: Money
)
The challenge I'm struggling with is how to annotate things so that the Money object can be given two different fields (currency and value) that come from the same level as the parent account.
Do I need to create an intermediate "unmarshalling" object called, say, MoshiAccount and then use a custom adapter to convert that to my real Account object?
I saw How to deseralize an int array into a custom class with Moshi? which looks close (except that in that case, the adapted object (VideoSize) only needs a single field as input... in my case, I need both currency and value)
Any thoughts or suggestions would be much appreciated. Thanks
Moshi's adapters can morph your JSON structure for you.
object ADAPTER {
private class FlatAccount(
val name: String,
val currency: String,
val value: Int
)
#FromJson private fun fromJson(json: FlatAccount): Account {
return Account(json.name, Money(json.currency, json.value))
}
#ToJson private fun toJson(account: Account): FlatAccount {
return FlatAccount(account.name, account.money.currency, account.money.value)
}
}
Don't forget to add the adapter to your Moshi instance.
val moshi = Moshi.Builder().add(Account.ADAPTER).add(KotlinJsonAdapterFactory()).build()
val adapter = moshi.adapter(Account::class.java)