How can I do a recursive / deep merge of two data classes in Kotlin? Something like this:
import kotlin.reflect.*
import kotlin.reflect.full.*
data class Address(
val street: String? = null,
val zip: String? = null
)
data class User(
val name: String? = null,
val age: Int? = null,
val address: Address? = null
)
inline fun <reified T : Any> T.merge(other: T): T {
val nameToProperty = T::class.declaredMemberProperties.associateBy { it.name }
val primaryConstructor = T::class.primaryConstructor!!
val args = primaryConstructor.parameters.associate { parameter ->
val property = nameToProperty[parameter.name]!!
val type = property.returnType.classifier as KClass<*>
if (type.isData) {
parameter to this.merge(other) //inline function can't be recursive
} else {
parameter to (property.get(other) ?: property.get(this))
}
}
return primaryConstructor.callBy(args)
}
val u1 = User(name = "Tiina", address = Address(street = "Hämeenkatu"))
val u2 = User(age = 23, address = Address(zip = "33100"))
u1.merge(u2)
// expected: User(age = 23, name= "Tiina", address = Address(zip = "33100", street = "Hämeenkatu")
related: Combining/merging data classes in Kotlin
There were several problems in the posted code,
unnecessary reification and inlining
when type isData was detected instead of merging the values of the property merge on this with the other was called, so it became endless recursion.
get cannot be used on KProperty1<out T, Any?> because of the variance
some non-idiomatic stuff which works, but can be made better
Here's the fixed version. For production I would've added some checks and error messages, but this should work for "happy path" and hopefully give you the base to build on:
import kotlin.reflect.KClass
import kotlin.reflect.KParameter
import kotlin.reflect.KProperty1
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.isSubclassOf
import kotlin.reflect.full.primaryConstructor
data class Address(
val street: String? = null,
val zip: String? = null
)
data class User(
val name: String? = null,
val age: Int? = null,
val address: Address? = null,
val map: Map<String, Int>? = null
)
fun <T> mergeData(property: KProperty1<out T, Any?>, left: T, right: T): Any? {
val leftValue = property.getter.call(left)
val rightValue = property.getter.call(right)
return rightValue?.let {
if ((property.returnType.classifier as KClass<*>).isSubclassOf(Map::class)) (leftValue as? Map<*, *>)?.plus(it as Map<*, *>)
else leftValue?.merge(it)
} ?: rightValue ?: leftValue
}
fun <T> lastNonNull(property: KProperty1<out T, Any?>, left: T, right: T) =
property.getter.call(right) ?: property.getter.call(left)
fun <T : Any> T.merge(other: T): T {
val nameToProperty = this::class.declaredMemberProperties.associateBy { it.name }
val primaryConstructor = this::class.primaryConstructor!!
val args: Map<KParameter, Any?> = primaryConstructor.parameters.associateWith { parameter ->
val property = nameToProperty[parameter.name]!!
val type = property.returnType.classifier as KClass<*>
when {
type.isData || type.isSubclassOf(Map::class) -> mergeData(property, this, other)
else -> lastNonNull(property, this, other)
}
}
return primaryConstructor.callBy(args)
}
// verification
val u1 = User(name = "Tiina", address = Address(street = "Hämeenkatu"), map = mapOf("a" to 1))
val u2 = User(age = 23, address = Address(zip = "33100"), map = mapOf("b" to 2))
check(
u1.merge(u2) == User(
age = 23,
name = "Tiina",
address = Address(zip = "33100", street = "Hämeenkatu"),
map = mapOf("a" to 1,"b" to 2)
)
) {
"doesn't work"
}
println("Works!")
Related
When implementing a REST API with Ktor (and Kotlin), it supports the optional field handling of Kotlin. Which works for POST and GET, but what about PATCH (update)?
For example, you have the following resource:
#Serializable
data class MyAddress(
var line1: String? = null,
var line2: String? = null,
var city: String? = null,
var postal_code: String? = null,
var state: String? = null,
var country: String? = null
)
So all MyAddress fields are optional (with a default value).
When you create an address with POST:
"line1" : "line1",
"country" : "XX"
and you than want to remove the country with a PATCH:
"country" : null
the end result of the resource should be:
"line1" : "line1"
But how can you determine this by parsing the json of the PATCH request? Because there is no way, as far as I know, to determine if it was null by default, or submitted.
Furthermore, the default null value for the MyAddress is required, because else the parsing will not work.
Code example:
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
#kotlinx.serialization.Serializable
data class MyAddress(
var line1: String? = null,
var line2: String? = null,
var city: String? = null,
var postal_code: String? = null,
var state: String? = null,
var country: String? = null
)
fun main() {
val jsonStringPOST = "{\"line1\":\"street\",\"country\":\"GB\"}"
println("JSON string is: $jsonStringPOST")
val myAddressPost = Json.decodeFromString<MyAddress>(jsonStringPOST)
println("MyAddress object: $myAddressPost")
val jsonStringPATCH = "{\"country\":null}"
println("JSON string is: $jsonStringPATCH")
val myAddressPatch = Json.decodeFromString<MyAddress>(jsonStringPATCH)
println("MyAddress object: $myAddressPatch")
}
I tried to add Optional<String>? as well, but it complains about missing serialization of Optional, and preferably I do not want to make all my data var's Optionals.
Note: I am looking for a more structured solution, that also works with all other resources in the api (10+ classes).
A second solution, based on Aleksei's example:
#Serializable
data class Address2(val line1: OptionalValue<String> = Undefined, val country: OptionalValue<String> = Undefined)
#Serializable(with = OptionalValueSerializer::class)
sealed interface OptionalValue<out T>
object Undefined: OptionalValue<Nothing> {
override fun toString(): String = "Undefined"
}
object Absent: OptionalValue<Nothing> {
override fun toString(): String = "Absent"
}
class WithValue<T>(val value: T): OptionalValue<T> {
override fun toString(): String = value.toString()
}
open class OptionalValueSerializer<T>(private val valueSerializer: KSerializer<T>) : KSerializer<OptionalValue<T>> {
override val descriptor: SerialDescriptor = valueSerializer.descriptor
override fun deserialize(decoder: Decoder): OptionalValue<T> {
return try {
WithValue(valueSerializer.deserialize(decoder))
} catch (cause: SerializationException) {
Absent
}
}
override fun serialize(encoder: Encoder, value: OptionalValue<T>) {
when (value) {
is Undefined -> {}
is Absent -> { encoder.encodeNull() }
is WithValue -> valueSerializer.serialize(encoder, value.value)
}
}
}
fun main() {
val jsonStringPOST = "{\"line1\":\"street\",\"country\":\"GB\"}"
println("JSON string is: $jsonStringPOST")
val myAddressPost = Json.decodeFromString<Address2>(jsonStringPOST)
println("MyAddress object: $myAddressPost")
val jsonStringUPDATE = "{\"country\":null}"
println("JSON string is: $jsonStringUPDATE")
val myAddressUpdate = Json.decodeFromString<Address2>(jsonStringUPDATE)
println("MyAddress object: $myAddressUpdate")
if(myAddressUpdate.country is Absent || myAddressUpdate.country is WithValue) {
println("Update country: ${myAddressUpdate.country}")
} else {
println("No update for country: ${myAddressUpdate.country}")
}
}
Output is:
JSON string is: {"line1":"street","country":"GB"}
MyAddress object: Address2(line1=street, country=GB)
JSON string is: {"country":null}
MyAddress object: Address2(line1=Undefined, country=Absent)
Update country: Absent
You can use a sealed interface for a part of an address to represent undefined value, absence of value, and actual value. For this interface, you need to write a serializer that will encode and decode values accordingly to your logic. I'm not good at the kotlinx.serialization framework so I wrote an example as simple as possible.
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.request.*
import io.ktor.server.routing.*
import kotlinx.serialization.*
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
fun main() {
embeddedServer(Netty, port = 4444) {
install(ContentNegotiation) {
json()
}
routing {
post {
val address = call.receive<Address>()
println(address)
}
}
}.start()
}
#Serializable
data class Address(val line1: MyValue = Undefined, val country: MyValue = Undefined)
#Serializable(with = AddressValueSerializer::class)
sealed interface MyValue
object Undefined: MyValue {
override fun toString(): String = "Undefined"
}
object Absent: MyValue {
override fun toString(): String = "Absent"
}
class WithValue(val value: String): MyValue {
override fun toString(): String = value
}
object AddressValueSerializer: KSerializer<MyValue> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("AddressValue", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): MyValue {
return try {
WithValue(decoder.decodeString())
} catch (cause: SerializationException) {
Absent
}
}
#OptIn(ExperimentalSerializationApi::class)
override fun serialize(encoder: Encoder, value: MyValue) {
when (value) {
is Undefined -> {}
is Absent -> { encoder.encodeNull() }
is WithValue -> { encoder.encodeString(value.value) }
}
}
}
With some help from medium.com, I came to the following solution:
#Serializable(with = OptionalPropertySerializer::class)
open class OptionalProperty<out T> {
object NotPresent : OptionalProperty<Nothing>()
data class Present<T>(val value: T) : OptionalProperty<T>() {
override fun toString(): String {
return value.toString()
}
}
fun isPresent() : Boolean {
return this is Present
}
fun isNotPresent(): Boolean {
return this is NotPresent
}
fun isEmpty(): Boolean {
return (this is Present) && this.value == null
}
fun hasValue(): Boolean {
return (this is Present) && this.value != null
}
override fun toString(): String {
if(this is NotPresent) {
return "<NotPresent>"
}
return super.toString()
}
}
open class OptionalPropertySerializer<T>(private val valueSerializer: KSerializer<T>) : KSerializer<OptionalProperty<T>> {
override val descriptor: SerialDescriptor = valueSerializer.descriptor
override fun deserialize(decoder: Decoder): OptionalProperty<T> =
OptionalProperty.Present(valueSerializer.deserialize(decoder))
override fun serialize(encoder: Encoder, value: OptionalProperty<T>) {
when (value) {
is OptionalProperty.NotPresent -> {}
is OptionalProperty.Present -> valueSerializer.serialize(encoder, value.value)
}
}
}
#Serializable
private data class MyAddressNew(
var line1: OptionalProperty<String?> = OptionalProperty.NotPresent,
var line2: OptionalProperty<String?> = OptionalProperty.NotPresent,
var city: OptionalProperty<String?> = OptionalProperty.NotPresent,
var postal_code: OptionalProperty<String?> = OptionalProperty.NotPresent,
var state: OptionalProperty<String?> = OptionalProperty.NotPresent,
var country: OptionalProperty<String?> = OptionalProperty.NotPresent,
)
fun main() {
val jsonStringPOST = "{\"line1\":\"street\",\"country\":\"GB\"}"
println("JSON string is: $jsonStringPOST")
val myAddressPost = Json.decodeFromString<MyAddressNew>(jsonStringPOST)
println("MyAddress object: $myAddressPost")
val jsonStringUPDATE = "{\"country\":null}"
println("JSON string is: $jsonStringUPDATE")
val myAddressUpdate = Json.decodeFromString<MyAddressNew>(jsonStringUPDATE)
println("MyAddress object: $myAddressUpdate")
if(myAddressUpdate.country.isPresent()) {
println("Update country: ${myAddressUpdate.country}")
} else {
println("No update for country: ${myAddressUpdate.country}")
}
}
This prints:
JSON string is: {"line1":"street","country":"GB"}
MyAddress object: MyAddressNew(line1=street, line2=<NotPresent>, city=<NotPresent>, postal_code=<NotPresent>, state=<NotPresent>, country=GB)
JSON string is: {"country":null}
MyAddress object: MyAddressNew(line1=<NotPresent>, line2=<NotPresent>, city=<NotPresent>, postal_code=<NotPresent>, state=<NotPresent>, country=null)
Update country: null
I have a question becouse i wrote this with a guide and it displays me all of the documents and i wanna that it displays only this with one i login in to my app and its will be nice when this wil be in 1 line and no i 2 seperates but it is not so necessary
val imie="Sebastian"
val query = db.collection("users").whereEqualTo("welcome",imie).orderBy("displayName")
val options = FirestoreRecyclerOptions.Builder<User>().setQuery(query, User::class.java)
.setLifecycleOwner(this).build()
val adapter = object: FirestoreRecyclerAdapter<User, UserViewHolder>(options) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
val view = LayoutInflater.from(this#MainActivity).inflate(android.R.layout.simple_list_item_2,parent,false)
return UserViewHolder(view)
}
override fun onBindViewHolder(holder: UserViewHolder, position: Int, model: User) {
val tvWelcome: TextView=holder.itemView.findViewById(android.R.id.text1)
val tvName: TextView=holder.itemView.findViewById(android.R.id.text2)
tvWelcome.text = model.welcome
tvWelcome.text = model.displayName
tvName.text = model.displayName
}
}
val uzytkownik: RecyclerView = findViewById(R.id.uzytkownik)
uzytkownik.adapter = adapter
uzytkownik.layoutManager = LinearLayoutManager(this)
Firebase console
val query = db.collection("users").whereEquelTo("firebasefieldName",youvalue)
Youvalue will write the data of the person you want to see.
eg:"Sebastian"
firebasefieldName will be your domain name in firebase
eg:displayName
Additional Information:
db.collection("users").whereEquelTo("fieldName1",youvalue).orderBy("fieldName2")
lists the listing in descending order of equal value
but after doing that you will get an error message there will be a link to that error message go to it and confirm everything.
Please support if it worked.
In this way, the data is updated instantly, for example, add a data to the table and it will appear in the list directly.
You can adjust it yourself.
Looks like I can't help any other way either. Will you still vote for me?
fun realtimeList(
collectionPath: String,
context: Context,
cevapField: String,
cevapdurum: Boolean,
field: String,
equel: String,
query: Query.Direction
): LiveData<MutableList<Any>> {
val mutableData = MutableLiveData<MutableList<Any>>()
val docRef = Firebase.firestore.collection(collectionPath)
docRef
.whereEqualTo(field, equel)
.whereEqualTo(cevapField, cevapdurum)
.orderBy("date", query)
.addSnapshotListener { querySnapshot, firebaseFirestoreException ->
firebaseFirestoreException?.let {
context.showShortToast(it.message.toString())
return#addSnapshotListener
}
val listData: MutableList<Any> = mutableListOf()
querySnapshot?.let {
for (document in it) {
val polyclinic = document.getString("policinic")
val title = document.getString("title")
val description = document.getString("descripiton")
val date: Long = document.get("date") as Long
val userid = document.getString("userid")
val quetinid = document.id
val cevapDurum = document.get("cevapdurum")
val questionio =
Questionio(
polyclinic!!,
title!!,
description!!,
SimpleDateFormat(date, "dd MMMM yyyy"),
SimpleDateFormat(date, "HH:mm"),
userid!!,
quetinid,
cevapDurum as Boolean
)
listData.add(questionio)
}
mutableData.value = listData
}
}
return mutableData
}
Part of my list adapter class
inner class ListViewHolder(itemView: View) :
RecyclerView.ViewHolder(itemView) {
fun bindView(questionio: Questionio) {
itemView.findViewById<TextView>(R.id.txt_policlinic).text = questionio.policlinic
itemView.findViewById<TextView>(R.id.txt_title).text = questionio.title
itemView.findViewById<TextView>(R.id.txt_description).text = questionio.description
itemView.findViewById<TextView>(R.id.txt_date).text = questionio.date
itemView.findViewById<TextView>(R.id.txt_time).text = questionio.time
val txtCevapDurum = itemView.findViewById<TextView>(R.id.txt_CevapDurum)
if (questionio.cevapdurum == true) {
txtCevapDurum.setText(R.string.answered)
txtCevapDurum.setTextColor(Color.GREEN)
} else {
txtCevapDurum.setText(R.string.not_answered)
txtCevapDurum.setTextColor(Color.RED)
}
}
init {
itemView.setOnClickListener {
listener.onItemClick(adapterPosition)
}
}
}
videos
https://www.youtube.com/watch?v=TIub9JMYhDs&t=226s&ab_channel=PhilippLackner
https://www.youtube.com/watch?v=Lfum5cO8LHA&ab_channel=SociedadAndroide
Using the sample below (based on this source but with some changes), if the property #Id is named "id", the Neo4J (latest version) does not recognizes the attribute when the load is executed. But if I named it to any other name, such as "myId", the method works. Please, does anyone know why?
My code:
package sample
import org.neo4j.ogm.annotation.Id
import org.neo4j.ogm.annotation.NodeEntity
import org.neo4j.ogm.annotation.Relationship
import org.neo4j.ogm.config.Configuration
import org.neo4j.ogm.session.SessionFactory
#NodeEntity
data class Actor(
#Id
var id: Long?,
var name:String
) {
constructor() : this(-1, "")
#Relationship(type = "ACTS_IN", direction = "OUTGOING")
var movies = mutableSetOf<Movie>()
fun actsIn(movie: Movie) {
movies.add(movie)
movie.actors.add(this)
}
}
#NodeEntity
data class Movie(
#Id
var id: Long?,
var title:String,
var released:Int
) {
constructor() : this(-1L, "", -1)
#Relationship(type = "ACTS_IN", direction = "INCOMING")
var actors = mutableSetOf<Actor>()
}
class Sample {
private val uri = "bolt://localhost:7687"
val configuration = Configuration.Builder()
.uri(uri)
//.credentials("neo4j", "test")
.build()
val sessionFactory = SessionFactory(configuration, "sample")
fun save() {
val pack = SessionFactory::class.java.name
val l = java.util.logging.Logger.getLogger(pack)
l.setLevel(java.util.logging.Level.ALL)
val session = sessionFactory.openSession()
val develsAdvocate = Movie(1, "The Devil’s Advocate", 1997)
val theMatrix = Movie(2, "The Matrix", 1999)
val act = Actor(1, "Carrie-Anne Moss")
act.actsIn(theMatrix)
val keanu = Actor(2, "Keanu Reeves")
keanu.actsIn(theMatrix)
keanu.actsIn(develsAdvocate)
session.save(develsAdvocate)
session.save(theMatrix)
session.clear()
// ** Only works if the entity has a attribute #Id with name different as "id"
val theMatrixReloaded = session.load(Movie::class.java, theMatrix.id, 2)
println("Found ${theMatrixReloaded.title}")
for (actor in theMatrixReloaded.actors) {
println("Actor ${actor.name} acted in ${actor.movies.joinToString { it.title }}")
}
sessionFactory.close()
}
}
fun main(args: Array<String>) {
val neoInstance = Sample()
neoInstance.save()
}
I'm facing this error when putting data into room from my api:
java.lang.RuntimeException: Unable to invoke no-args constructor for
retrofit2.Call<com.example.youbank.models.Customer>. Registering an
InstanceCreator with Gson for this type may fix this problem.
I have looked up this issue and tried multiple things to fix it but, i think there is something else wrong, which probably comes down to my lack of knowledge on this subject.
Sorry for the big copypaste of my code but i don't know where to fix this problem so im just including what i think is needed.
HomeScreenFragment:
class HomeScreenMotionFragment: Fragment(), CoroutineScope {
private var job: Job = Job()
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + job
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
private var _binding: FragmentHomeScreenMotionBinding? = null
private val binding get() = _binding!!
private val vm: CustomerViewModel by activityViewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
launch {
vm.addCustomerToRoomDB(14)
}
}
CustomerViewModel:
class CustomerViewModel(application: Application): AndroidViewModel(application) {
val readCustomer: LiveData<RoomCustomer>
val readAccount: LiveData<List<RoomAccount>>
val readCard: LiveData<List<RoomCard>>
private val customerRepo: CustomerRepository
private val accountRepo: AccountRepository
private val cardRepo: CardRepository
var cus: Customer
var a = listOf<Account>()
var cards = listOf<Card>()
init {
cus = Customer()
val customerDao = CustomerDatabase.getDatabase(application).customerDao()
customerRepo = CustomerRepository(customerDao)
readCustomer = customerRepo.readCustomer
val accountDao = CustomerDatabase.getDatabase(application).accountDao()
accountRepo = AccountRepository(accountDao)
readAccount = accountRepo.readAccounts
val cardDao = CustomerDatabase.getDatabase(application).cardDao()
cardRepo = CardRepository(cardDao)
readCard = cardRepo.readCards
}
suspend fun addCustomerToRoomDB(id: Int) {
val service: CustomerService = ApiService.buildService(CustomerService::class.java)
val req: Call<Customer> = service.getCustomerById(id)
req.enqueue(object: Callback<Customer> {
override fun onResponse(call: Call<Customer>, response: Response<Customer>) {
cus = response.body()!!
a = response.body()!!.accounts
cards = response.body()!!.accounts[0].cards
}
override fun onFailure(call: Call<Customer>, t: Throwable) {
Log.d("get customer failed", t.cause.toString())
}
})
val roomCustomer = RoomCustomer(
0, cus.customerId, cus.fullName, cus.phone, cus.address, cus.birthday.toString(), cus.email, cus.password)
val roomAccount = RoomAccount(0, a[0].accountId, a[0].accountNumber, a[0].accountType, a[0].balance)
val roomCard = RoomCard(
0, cards[0].cardId, cards[0].cardNumber, cards[0].ccv, cards[0].expirationDate, cards[0].cardType,
cards[0].cardStatus)
// Adding customer to roomdatabase
customerRepo.addCustomer(roomCustomer)
accountRepo.addAccounts(roomAccount)
cardRepo.addCards(roomCard)
}
}
CustomerService:
interface CustomerService {
#GET("Customers/{id}")
suspend fun getCustomerById(#Path("id") id: Int): Call<Customer>
}
My models:
class Customer {
var customerId: Int = -1
var fullName: String = ""
var phone: String = ""
var address: String = ""
var birthday: Date? = null
var email: String = ""
var password: String = ""
lateinit var accounts: List<Account>
}
class Account {
var accountId: Int = -1
var accountNumber: String = generateAccNumber()
lateinit var accountType: AccountType
var balance: Double = 0.0
lateinit var cards: List<Card>
}
class Card {
var cardId: Int = -1
var cardNumber: Int = -1
var ccv: Int = -1
lateinit var expirationDate: String
lateinit var cardType: CardType
lateinit var cardStatus: CardStatus
}
My repositories:
class CustomerRepository(private val customerDao: CustomerDao) {
val readCustomer: LiveData<RoomCustomer> = customerDao.getCustomer()
fun addCustomer(c: RoomCustomer) {
customerDao.addCustomer(c)
}
}
class AccountRepository (private val accountDao: AccountDao) {
val readAccounts: LiveData<List<RoomAccount>> = accountDao.getAccounts()
fun addAccounts(a: RoomAccount) {
accountDao.addAccount(a)
}
}
class CardRepository(private val cardDao: CardDao) {
val readCards: LiveData<List<RoomCard>> = cardDao.getCards()
fun addCards(c: RoomCard) {
cardDao.addCards(c)
}
}
My daos:
#Dao
interface CustomerDao {
#Query("SELECT * FROM customer_table")
fun getCustomer(): LiveData<RoomCustomer>
#Insert
fun addCustomer(c: RoomCustomer)
#Delete
fun deleteCustomer(c: RoomCustomer)
}
#Dao
interface AccountDao {
#Query("SELECT * FROM accounts_table")
fun getAccounts(): LiveData<List<RoomAccount>>
#Insert
fun addAccount(a: RoomAccount)
#Delete
fun deleteAccount(a: RoomAccount)
}
#Dao
interface CardDao {
#Query("SELECT * FROM cards_table")
fun getCards(): LiveData<List<RoomCard>>
#Insert
fun addCards(c: RoomCard)
#Delete
fun deleteCard(c: RoomCard)
}
My room models:
#Entity(tableName = "customer_table")
data class RoomCustomer(
#PrimaryKey(autoGenerate = true)
val CID: Int,
val customerId: Int,
val fullName: String,
val phone: String,
val address: String,
val birthday: String,
val email: String,
val password: String
)
#Entity(tableName = "accounts_table")
data class RoomAccount(
#PrimaryKey(autoGenerate = true)
val AID: Int,
val accountId: Int,
val accountNumber: String,
val accountType: AccountType,
val balance: Double
)
#Entity(tableName = "cards_table")
data class RoomCard(
#PrimaryKey(autoGenerate = true)
val CID: Int,
val cardId: Int,
val cardNumber: Int,
val ccv: Int,
val expirationDate: String,
val cardType: CardType,
val cardStatus: CardStatus
)
Let's say I'm having a class like:
#Serializable
data class MyClass(
#SerialName("a") val a: String?,
#SerialName("b") val b: String
)
Assume the a is null and b's value is "b value", then Json.stringify(MyClass.serializer(), this) produces:
{ "a": null, "b": "b value" }
Basically if a is null, I wanted to get this:
{ "b": "b value" }
From some research I found this is currently not doable out of the box with Kotlinx Serialization so I was trying to build a custom serializer to explicitly ignore null value. I followed the guide from here but couldn't make a correct one.
Can someone please shed my some light? Thanks.
You can use explicitNulls = false
example:
#OptIn(ExperimentalSerializationApi::class)
val format = Json { explicitNulls = false }
#Serializable
data class Project(
val name: String,
val language: String,
val version: String? = "1.3.0",
val website: String?,
)
fun main() {
val data = Project("kotlinx.serialization", "Kotlin", null, null)
val json = format.encodeToString(data)
println(json) // {"name":"kotlinx.serialization","language":"Kotlin"}
}
https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/json.md#explicit-nulls
Use encodeDefaults = false property in JsonConfiguration and it won't serialize nulls (or other optional values)
Try this (not tested, just based on adapting the example):
#Serializable
data class MyClass(val a: String?, val b: String) {
#Serializer(forClass = MyClass::class)
companion object : KSerializer<MyClass> {
override val descriptor: SerialDescriptor = object : SerialClassDescImpl("MyClass") {
init {
addElement("a")
addElement("b")
}
}
override fun serialize(encoder: Encoder, obj: MyClass) {
encoder.beginStructure(descriptor).run {
obj.a?.let { encodeStringElement(descriptor, 0, obj.a) }
encodeStringElement(descriptor, 1, obj.b)
endStructure(descriptor)
}
}
override fun deserialize(decoder: Decoder): MyClass {
var a: String? = null
var b = ""
decoder.beginStructure(descriptor).run {
loop# while (true) {
when (val i = decodeElementIndex(descriptor)) {
CompositeDecoder.READ_DONE -> break#loop
0 -> a = decodeStringElement(descriptor, i)
1 -> b = decodeStringElement(descriptor, i)
else -> throw SerializationException("Unknown index $i")
}
}
endStructure(descriptor)
}
return MyClass(a, b)
}
}
}
Since I was also struggling with this one let me share with you the solution I found that is per property and does not require to create serializer for the whole class.
class ExcludeIfNullSerializer : KSerializer<String?> {
override fun deserialize(decoder: Decoder): String {
return decoder.decodeString()
}
override val descriptor: SerialDescriptor
get() = PrimitiveSerialDescriptor("ExcludeNullString", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: String?) {
if (value != null) {
encoder.encodeString(value)
}
}
}
will work as expected with the following class
#Serializable
class TestDto(
#SerialName("someString")
val someString: String,
#SerialName("id")
#EncodeDefault(EncodeDefault.Mode.NEVER)
#Serializable(with = ExcludeIfNullSerializer::class)
val id: String? = null
)
Note the #EncodeDefault(EncodeDefault.Mode.NEVER) is crucial here in case you using JsonBuilder with encodeDefaults = true, as in this case the serialization library will still add the 'id' json key even if the value of id field is null unless using this annotation.
JsonConfiguration is deprecated in favor of Json {} builder since kotlinx.serialization 1.0.0-RC according to its changelog.
Now you have to code like this:
val json = Json { encodeDefaults = false }
val body = json.encodeToString(someSerializableObject)
As of now, for anyone seeing this pos today, default values are not serialized (see https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/basic-serialization.md#defaults-are-not-encoded-by-default)
So you simply add to set a default null value, and it will not be serialized.