Kotlin Serialization #SerialName not working for Boolean - android

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)

Related

Android data class structure for unlabelled API response

An API is returning responses without any outlying data fields, but instead only as an array object like this:
[{"name":"John Smith", "age":"44", "address":"1 Main Street, Anywhere, USA"}, {"name":"Jane Taylor", "age":"22", "address":"10 Suburbia Lane, Sometown, USA"},{"name":"Simon Jones", "age":"36", "address":"33 City Boulevard, Midvalley, USA"}]
My response data classes usually include labels, like this:
data class MyResponseClass (
val status: String? = null,
val description: String? = null
)
How should I structure a response class that does not include labels?
TIA.
UPDATE:
Following Ivo's answer (thank you, btw), how could such a class inherit a base class?
open class MyBaseReponseClass() : Serializable {
val status: String? = null
val description: String? = null
}
data class MyResponseClass (
val name: String? = null,
val age: String? = null,
val address: String? = null
) : MyBaseReponseClass()
The name, age, and address are in an array, but not status and description.
Thanks again!
Not sure what you mean with not using labels. it clearly has name, age and address so it will be
data class MyResponseClass (
val name: String? = null,
val age: String? = null
val address: String? = null
)
And where you indicate the type of the response you say it's a List<MyResponseClass>

Creating Domain Model mapper with complex DTO

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.

How to accomodate different arguments for entity class

I am calling my entity class in an if/else-if statement for 2 different conditions and the values I pass into the parameters depend on the condition. In the if block, I am passing 3 parameters and in the else-if block, I am passing 4. The entity's object is throwing an error because it is expecting 4 parameters. I want the first parameter to be optional and I'd like to know if there is a way to do that in Kotlin.
This is my entity class:
#Entity(tableName = "entry_table")
data class Entry(
#PrimaryKey(autoGenerate = true)
var id: Int? = null,
val username: String? = null,
val hint: String? = null,
val password: String? = null)
And this is the if/else-if block where I'm inserting values into the entity object:
if (requestCode == ADD_ENTRY_REQUEST && resultCode == Activity.RESULT_OK) {
...
val entry = Entry(username, hint, password)
...
} else if (requestCode == EDIT_ENTRY_REQUEST && resultCode == Activity.RESULT_OK) {
...
val entry = Entry(id, username, hint, password)
...
}
In Java, you could solve this problem by creating 2 constructors with matching number of parameters but I wonder if we can do the same in Kotlin or if there is a different approach.
You can try moving the id to the end, like so:
#Entity(tableName = "entry_table")
data class Entry(
val username: String? = null,
val hint: String? = null,
val password: String? = null,
#PrimaryKey(autoGenerate = true)
var id: Int? = null)
And then creating it like:
val entry = Entry(username, hint, password, id)
Or, if you want to keep the id as the first parameter, you can use named arguments like this:
val entry = Entry(username = username, hint = hint, password = password)
Hope that helps!
You can and probably should use multiple constructors.
The downside with your approach is that all the properties must be optional for technical reasons. But I guess, username and password actually are mandatory. The consequences are loss of compile time checks (and therefore prone to NPEs) and cumbersome reading (!! needed when accessing username).
You can avoid these problems following this approach:
To make your class have the default no-arg constructor, that is required by JPA, use the compiler plugin https://kotlinlang.org/docs/reference/compiler-plugins.html#jpa-support
Make mandatory properties non-nullable (I assume, only hint is optional)
Use as many secondary constructors as you like
#Entity(tableName = "entry_table")
data class Entry(
#PrimaryKey(autoGenerate = true)
val id: Int,
val username: String,
val hint: String?,
val password: String
) {
constructor(
username: String,
hint: String = null,
password: String = "change it"
) : this(0, username, hint, password)
}
In a more realistic scenario, only id and username were immutable. password and hint should be allowed to change.
#Entity(tableName = "entry_table")
data class Entry(
#PrimaryKey(autoGenerate = true)
val id: Int,
val username: String
) {
constructor(
username: String,
) : this(0, username)
var hint: String? = null
var password: String = "change it"
}

Deserialize a Firebase Data-Snapshot to a Kotlin data class

Hi I have a Kotlin data class as follows
data class User (
#get:Exclude val gUser: Boolean,
#get:Exclude val uid: String,
#get:PropertyName("display_name") val displayName: String,
#get:PropertyName("email") val email: String,
#get:PropertyName("account_picture_url") val accountPicUrl: String,
#get:PropertyName("provider") val provider: String
)
I am able to serialize the object without an issues. But i'm having trouble deserializing the object when doing a firebase query. Currently this is what i'm doing to get the data
_firebaseReference.child(getString(R.string.firebase_users_key)).child(user.uid)
.setValue(user).addOnCompleteListener{
_firebaseReference.child("users").child(user.uid)
.addListenerForSingleValueEvent(object : ValueEventListener {
override fun onCancelled(p0: DatabaseError) {
}
override fun onDataChange(p0: DataSnapshot) {
if (p0.exists()) {
val userHash = p0.value as HashMap<*, *>
var currentUser: User
if (userHash[getString(R.string.provider_key)]
!= getString(R.string.provider_google)) {
currentUser = User(false, p0.key!!,
userHash["display_name"].toString(),
userHash["email"].toString(),
userHash["account_picture_url"].toString(),
userHash["provider"].toString())
} else {
currentUser = User(true, p0.key!!,
userHash["display_name"].toString(),
userHash["email"].toString(),
userHash["account_picture_url"].toString(),
userHash["provider"].toString())
}
}
}
})
}
This is only a test project that i'm working on to practice my Kotlin, but this is something I would like to figure out.
If i'm doing it completely wrong please let me know, any advise would be greatly appreciated
Thanks
Firebase needs an empty constructor to be able to deserialize the objects:
data class User(
#Exclude val gUser: Boolean,
#Exclude val uid: String,
#PropertyName("display_name") val displayName: String,
#PropertyName("email") val email: String,
#PropertyName("account_picture_url") val accountPicUrl: String,
#PropertyName("provider") val provider: String
) {
constructor() : this(false, "", "", "", "", "")
}
You can either declare it like so and provide some default values to be able to call the primary constructor or you can declare default values for all your parameters:
data class User (
#Exclude val gUser: Boolean = false,
#Exclude val uid: String = "",
#PropertyName("display_name") val displayName: String = "",
#PropertyName("email") val email: String = "",
#PropertyName("account_picture_url") val accountPicUrl: String = "",
#PropertyName("provider") val provider: String = ""
)
Then various constructors will be created for you, including an empty constructor.
If there's a problem with serialization there might be because of the getters and setters generated by the ide, try reinforcing them with #get and #set annotations:
data class User (
#Exclude val gUser: Boolean = false,
#Exclude val uid: String = "",
#set:PropertyName("display_name")
#get:PropertyName("display_name")
var displayName: String = "",
#PropertyName("email") val email: String = "",
#set:PropertyName("account_picture_url")
#get:PropertyName("account_picture_url")
var accountPicUrl: String = "",
#PropertyName("provider") val provider: String = ""
)
What I actually wanted is a Kotlin data class which is derived from a domain model interface like so
data class Dto(#PropertyName("serialized_title") val override title: String) : DomainModel
In this case DomainModel is defined this way
interface DomainModel { val title: String }
My goal was to fetch data from Firestore and get deserialized Dto objects which are provided to clients which receive objects of type DomainModel. So this solution above unfortunately didn't work. I saw the workarounds using #get: and #set: Annotations but I wanted my data class properties to be immutable. Simply using vars is a bad design decision in my use case. And also this solution looks quite ugly...
After inspecting the decompiled Java-Code I came up with this solution
data class Dto(
#field:[JvmField PropertyName("serialized_title")]
override val title: String = "") : DomainModel
The decompiled Java-Code simply uses title as public final field having the PropertyName annotation.
I prefer this solution since it doesn't violate certain design decisions I made...
In Android Studio (kotlin)
use this (only var and getter and setter):
#set:PropertyName("email") #get:PropertyName("email") var emailPerson: String = ""
None of this works:
#PropertyName("email") var emailPerson: String = ""
#PropertyName("email") val emailPerson: String = ""
#get:PropertyName("email") val emailPerson: String = ""
Android Studio 4.1.2. Gradle: com.google.firebase:firebase-database:19.6.0

Customise single data class without creating multiple data classes in Kotlin

I have a scenario. I have created a data class in Kotlin like this:
data class AgentDetails(
val mobileNo: String,
val Name: String,
val Email: String,
val Password: String,
val Occupation: String,
val pincode: String,
val usertype: String,
val profilepic: String,
val AccountStatus: String
)
I want to send different type of objects of this data class to a web service:
1st object example:
val agentDetails = AgentDetails(mobileNo = mobileNumberText.text.toString(),
Name = userNameText.text.toString(),
Email = emailIdText.text.toString(),
Password = HashUtils.sha1(passwordText.text.toString()),
Occupation = item,
pincode = pinCodeText.text.toString(),
usertype = "Agent",
profilepic = "null", AccountStatus = "pending")
In 2nd object I only want to send mobile number. I dont wanna include any other field. Something like this:
val agentDetails = AgentDetails(mobileNo = mobileNumberText.text.toString())
And in 3rd object I only wanna send email id. Instead of creating multiple data classes. Can I use the same data class for multiple implementations?
Personally, I'd define three objects because they represent three different concepts (or projections of a concept). But if you make your properties nullable and provide a default value of null, you can get away with creating them as you want...
data class AgentDetails(
val mobileNo: String? = null,
val name: String? = null,
val email: String? = null,
val password: String? = null,
val occupation: String? = null,
val pincode: String? = null,
val usertype: String? = null,
val profilepic: String? = null,
val accountStatus: String? = null
)
Note: I've changed some of your property names to camelCase, as is the proper convention. And these all work fine:
AgentDetails(mobileNo = mobileNumberText.text.toString())
AgentDetails(email = "foo#example.com")
AgentDetails(name = "Foo", password = "Bar")
All of the other fields not provided will be null, and the types will be nullable, so you'll have to guard against that. Otherwise, I'd define three data classes for this.
Another solution would be to consider a sealed class structure:
sealed class AgentDetails
data class AgentByName(val name: String) : AgentDetails()
data class AgentByEmail(val email: String): AgentDetails()
// etc..
And then use it in a when expression:
fun doSomethingWithAgents(agentDetails: AgentDetails) {
when (agentDetails) {
is AgentByName -> // Do something
is AgentByEmail -> // Do Something
}
}
The easiest way is to make the fields nullable and provide default values:
data class AgentDetails(
val mobileNo: String? = null,
val Name: String? = null,
val Email: String? = null,
val Password: String? = null,
val Occupation: String? = null,
val pincode: String? = null,
val usertype: String? = null,
val profilepic: String? = null,
val AccountStatus: String? = null
)

Categories

Resources