I am using JSON to Kotlin plugin for generating DTO classes with Moshi to save a lot of time dealing with complex JSON response from APIs.
Just to give a glimpse how huge the response can be
Sample DTO using Moshi
#JsonClass(generateAdapter = true)
data class AssetDto(
#Json(name = "data")
val `data`: List<AssetData> = listOf(),
#Json(name = "status")
val status: Status = Status()
)
#Parcelize
#JsonClass(generateAdapter = true)
data class Status(
#Json(name = "elapsed")
val elapsed: Int = 0,
#Json(name = "timestamp")
val timestamp: String = ""
) : Parcelable
#Parcelize
#JsonClass(generateAdapter = true)
data class AssetData(
#Json(name = "id")
val id: String = "",
#Json(name = "metrics")
val metrics: Metrics = Metrics(),
#Json(name = "name")
val name: String = "",
#Json(name = "profile")
val profile: Profile = Profile(),
#Json(name = "symbol")
val symbol: String? = ""
) : Parcelable
Problems
I want to know what is the best way to create Domain Model out of a complex DTO class without manually encoding it.
Should I create Domain Model for AssetDto or AssetData? As you can see I have tons of value-object and I do not know if I should create a Domain Model on each of those value-object or it is okay to reuse the data class from DTO.
For now I generate another pile of plain data class using JSON to Kotlin which means I have dozens of identical data class and it looks like I am still oblige to manually set each values, this became a total blocker now. I am not sure if I should continue implementing mapper.
data class AssetDomain(
var status: Status? = Status(),
var `data`: List<Data>? = listOf()
)
data class Status(
var elapsed: Int? = 0,
var timestamp: String? = ""
)
data class Data(
var id: String? = "",
var metrics: Metrics? = Metrics(),
var name: String? = "",
var profile: Profile? = Profile(),
var symbol: String? = ""
)
You should create domain models based on your business logic not based on the response itself.
Related
I need to show this data in my sample kotlin app.
I have created all appropriate models using moshi. Which can be found here.
But the values are showing wrong. Even non null values are showing as null.
Example json, value in 0 index:
"main": {
"temp": 25.78,
"feels_like": 25.95,
"temp_min": 23.35,
"temp_max": 25.78,
"pressure": 1014,
"sea_level": 1014,
"grnd_level": 1012,
"humidity": 59,
"temp_kf": 2.43
},
The model responsible:
#Parcelize
#JsonClass(generateAdapter = true)
data class Main(
#Json(name = "temp")
val temp: Double?,
#Json(name = "feels_like")
val feelsLike: Double?,
#Json(name = "temp_min")
val tempMin: Double?,
#Json(name = "temp_max")
val tempMax: Double?,
#Json(name = "pressure")
val pressure: Double?,
#Json(name = "sea_level")
val seaLevel: Double?,
#Json(name = "grnd_level")
val grndLevel: Double?,
#Json(name = "humidity")
val humidity: Double?,
#Json(name = "temp_kf")
val tempKf: Double?,
) : Parcelable {
fun getTempString(): String {
return temp.toString().substringBefore(".") + "°"
}
fun getHumidityString(): String {
return humidity.toString() + "°"
}
fun getTempMinString(): String {
return tempMin.toString().substringBefore(".") + "°"
}
fun getTempMaxString(): String {
return tempMax.toString().substringBefore(".") + "°"
}
}
But when I print the main in 0 index shows:
Main(temp=298.26, feelsLike=null, tempMin=null, tempMax=null, pressure=1014.0, seaLevel=null, grndLevel=null, humidity=62.0, tempKf=null)
As a result the views are not working either.
The entire source code can be found here.
See discussion here, it might be that the following anotation #field:Json(name = "temp_min") instead of #Json(name = "temp_min"), and the same for the other members, could fix it. Alternatively, you can try to use a different version of Moshi. Also, see related discussion Moshi #Json annotation not working for com.github.kittinunf.fuel.moshi.moshiDeserializerOf?
I'm using GSON to serialize some platform data. When I use #SerialName to capture platform data with a different naming convention in my app, it works for other types, but not Boolean types. As a simple example, if I have a class like...
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
#Serializable
data class Person (
#SerialName("first_name") val firstName: String? = null,
#SerialName("last_name") val lastName: String? = null,
val age: Int? = null
)
... everything works fine. The serializer finds first_name, last_name and age in the data and properly set the properties for the Person.
However, when I try to add a Boolean...
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
#Serializable
data class Person (
#SerialName("first_name") val firstName: String? = null,
#SerialName("last_name") val lastName: String? = null,
val age: Int? = null,
#SerialName("can_sing") val canSing: Boolean? = null
)
... the serializer does not catch and assign can_sing. It is strange that it works with a String but not a Boolean. Can any explain why I am seeing this behavior? I can work around this (for example, I can do val can_sing: Boolean? = null and it works), but I'm just wonder why #SerialName doesn't seem to work for a Boolean, or if I'm just missing something obvious.
You are mixing the Gson and Kotlin annotation types - Gson uses #SerializedName not #SerialName. I am not sure how your string types even work in that case (maybe something in how you call Gson that isn't included in the question).
As an example, the first class here (Person) can be serialized with the Kotlin serialization library, the second with Gson:
Kotlin annotations
#Serializable
data class Person (
#SerialName("first_name") val firstName: String? = null,
#SerialName("last_name") val lastName: String? = null,
val age: Int? = null,
#SerialName("can_sing") val canSing: Boolean? = null
)
Gson Annotations
data class PersonGson (
#SerializedName("first_name") val firstName: String? = null,
#SerializedName("last_name") val lastName: String? = null,
val age: Int? = null,
#SerializedName("can_sing") val canSing: Boolean? = null
)
Examples
Running this unit test with the Kotlin serialization library:
#Test
fun testJsonKotlin() {
val test = Person("hello", "world", 42, false)
val json = Json.encodeToString(test)
println(json)
val t2 = Json.decodeFromString<Person>(json)
println(t2)
}
produces the expected output:
{"first_name":"hello","last_name":"world","age":42,"can_sing":false}
Person(firstName=hello, lastName=world, age=42, canSing=false)
Doing that with Gson
#Test
fun testJsonGsonMixed() {
val testp = Person("hello", "world", 42, false)
val json = Gson().toJson(testp)
println(json)
val t2 = Gson().fromJson(json, Person::class.java)
println(t2)
}
technically works, but ignores the serialized name annotations (for all the cases, not just the boolean)
{"firstName":"hello","lastName":"world","age":42,"canSing":false}
Person(firstName=hello, lastName=world, age=42, canSing=false)
Using the Gson-annotated class with Gson
#Test
fun testJsonGson() {
val test = PersonGson("hello", "world", 42, false)
val json = Gson().toJson(test)
println(json)
val t2 = Gson().fromJson(json, PersonGson::class.java)
println(t2)
}
gives the correct response again
{"first_name":"hello","last_name":"world","age":42,"can_sing":false}
PersonGson(firstName=hello, lastName=world, age=42, canSing=false)
Given that I have 3 entities, Order contains list of LineItem, each LineItem will associates with one Product by productId.
The problem that when I get data from OrderDao, it returns null for the product field, but in the lineItem field, it has data. While I can data with ProductWithLineItem.
Already tried a lot of work arounds but it does not work.
Here is my code for entities and dao
Entities
#Entity(tableName = DataConstant.ORDER_TABLE)
data class Order(
#PrimaryKey
#ColumnInfo(name = "orderId")
val id: String,
#ColumnInfo(name = "status")
var status: String
)
#Entity(tableName = DataConstant.LINE_ITEM_TABLE)
data class LineItem(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "lineItemId")
val id: Long,
#ColumnInfo(name = "productId")
val productId: String,
#ColumnInfo(name = "orderId")
val orderId: String,
#ColumnInfo(name = "quantity")
var quantity: Int,
#ColumnInfo(name = "subtotal")
var subtotal: Double
)
#Entity(tableName = DataConstant.PRODUCT_TABLE)
data class Product(
#PrimaryKey
#NonNull
#ColumnInfo(name = "productId")
val id: String,
#ColumnInfo(name = "name")
var name: String?,
#ColumnInfo(name = "description")
var description: String?,
#ColumnInfo(name = "price")
var price: Double?,
#ColumnInfo(name = "image")
var image: String?,
)
Relations POJOs
data class ProductAndLineItem(
#Embedded val lineItem: LineItem?,
#Relation(
parentColumn = "productId",
entityColumn = "productId"
)
val product: Product?
)
data class OrderWithLineItems(
#Embedded var order: Order,
#Relation(
parentColumn = "orderId",
entityColumn = "orderId",
entity = LineItem::class
)
val lineItemList: List<ProductAndLineItem>
)
Dao
#Dao
interface OrderDao {
#Transaction
#Query("SELECT * FROM `${DataConstant.ORDER_TABLE}` WHERE orderId = :id")
fun getById(id: String): Flow<OrderWithLineItems>
}
Result after running with Dao
Result after running query
Here is my code for entities and dao
You code appears to be fine, with the exception of returning a Flow, testing, using your code, but on the main thread using List (and no WHERE clause) i.e the Dao being :-
#Query("SELECT * FROM ${DataConstant.ORDER_TABLE}")
#Transaction
abstract fun getOrderWithLineItemsAndWithProduct(): List<OrderWithLineItems>
Results in :-
The data being loaded/tested using :-
db = TheDatabase.getInstance(this)
orderDao = db.getOrderDao()
orderDao.clearAll()
orderDao.insert(Product("product1","P1","desc1",10.01,"image1"))
orderDao.insert(Product("product2","P2","desc2",10.02,"image2"))
orderDao.insert(Product("product3","P3","desc3",10.03,"image3"))
orderDao.insert(Product("product4","P4","desc4",10.04,"image4"))
orderDao.insert(Product("","","",0.0,""))
val o1 = orderDao.insert(Order("Order1","initiaited"))
val o2 = orderDao.insert(Order("Order2","finalised")) // Empty aka no List Items
val o1l1 = orderDao.insert(LineItem(10,"product3","Order1",1,10.01))
val o1l2 = orderDao.insert(LineItem(20,"product4","Order1",2,20.08))
val o1l3 = orderDao.insert(LineItem(30,"","Order1",3,30.09))
val o1l4 = orderDao.insert(LineItem(40,"","x",1,10.01))
//val o1l3 = orderDao.insert(LineItem(30,"no such product id","Order1",10,0.0))
// exception whilst trying to extract if not commented out at test = ....
val TAG = "ORDERINFO"
val test = orderDao.getOrderWithLineItemsAndWithProduct()
for(owl: OrderWithLineItems in orderDao.getOrderWithLineItemsAndWithProduct()) {
Log.d(TAG,"Order is ${owl.order.id} status is ${owl.order.status}")
for(pal: ProductAndLineItem in owl.lineItemList) {
Log.d(TAG,"\tLine Item is ${pal.lineItem.id} " +
"for Order ${pal.lineItem.orderId} " +
"for ProductID ${pal.lineItem.productId} " +
"Quantity=${pal.lineItem.quantity} " +
"Product description is ${pal.product.description} Product Image is ${pal.product.image} Price is ${pal.product.price}")
}
}
As such I believe the issue might be that for some reason the Flow is detecting when the first query has completed but prior to the underlying queries.
That is when using #Relation the core objects (Order's) are extracted via the query and the core objects created then the related objects are extracted by a another query and used to build ALL the related objects as a List (unless just the one when it doesn't have to be a list). So prior to this underlying query the core object will have a null or an empty list for the underlying objects. Of course with a hierarchy of #Relations then this is replicated along/down the hierarchy.
I would suggest temporarily adding .allowMainThreadQueires to the databaseBuilder and using a List<OrderWithLineItems> or just a sole OrderWithLineItems. If using this then you get the Product(s) then the issue is with the Flow (which is what I suspect).
Apologies, I am fairly new to Kotlin!
What I have:
#entity(tableName = "main_table")
data class Table(
#PrimaryKey(autoGenerate = true) var tableId: Int,
#ColumnInfo(name = "column_name") val columnName: String?,
...
)
What I would like:
val columnList = listOf(...list of strings...)
#entity(tableName = "main_table")
data class Table(
#PrimaryKey(autoGenerate = true) var tableId: Int,
for (column in columnList)
#ColumnInfo(name = column+"_name") val column+Name: String?,
...
)
Motivation: that list will be re-used in other parts of the code, as such, it would be great if it only existed within the code once.
Two unknowns here for me, can that loop be done somehow? And can concatenation be done during the variable declaration?
Thank you for reading!
I got an entity for Android Room which looks like that. So far, no worries.
#Entity(tableName = "users",
indices = arrayOf(Index(value = "nickName", unique = true)))
data class User(#ColumnInfo(name = "nickName") var nickName: String,
#ColumnInfo(name = "password") var password: String) {
#ColumnInfo(name = "id")
#PrimaryKey(autoGenerate = true)
var id: Long = 0
}
Now I need to encrypt the password. With Java, this would simply be done with a setter and that would work.
How would you do that with Kotlin. I cannot find a solution to combine Android Room, custom setters and data classes.
You can try something like this:
#Entity(tableName = "users",
indices = arrayOf(Index(value = "nickName", unique = true)))
data class User(#ColumnInfo(name = "nickName") var nickName: String,
private var _password: String) {
#ColumnInfo(name = "id")
#PrimaryKey(autoGenerate = true)
var id: Long = 0
#ColumnInfo(name = "password")
var password: String = _password
set(value) {
field = "encrypted"
}
override fun toString(): String {
return "User(id=$id, nickName='$nickName', password='$password')"
}
}
But I wouldn't recommend encrypting password inside Entity or modifying it somehow as it isn't its responsibility and you may face errors with double encryption of your password as when you retrieve your entity from database Room will populate the entity with data which will lead to encryption of already encrypted data.
#Entity(tableName = "users",
indices = arrayOf(Index(value = "nickName", unique = true)))
data class User(#ColumnInfo(name = "nickName") var nickName: String,
#ColumnInfo(name = "password") var password: String) {
var _password = password
set(value): String{
//encrypt password
}
#ColumnInfo(name = "id")
#PrimaryKey(autoGenerate = true)
var id: Long = 0
}
This will create a custom setter so every time you set your password you can encrypt it as well inside the setter.