How to prevent serializing null values using kotlin serialization - kotlin

I'm using kotlinx-serialization-json
I have this class:
#Serializable
data class Event(
#SerialName("temperature") val temperature: Float?,
#SerialName("pressure") val pressure: Float?,
#SerialName("humidity") val humidity: Float?,
)
and this call
Json.encodeToString(Event(temperature = 42.0f, pressure = null, humidity = 20.9f))
During serialization I receive such json:
{
"temperature": 20.5,
"pressure": null,
"humidity": 20.9
}
but I would like to prevent serializing null values and receive this:
{
"temperature": 20.5,
"humidity": 20.9
}
It's a problem for me, because during serializing lengthy list of events I waste a lot of lines. Anyone's got an idea how to achieve this?
EDIT:
There is new simple way to achieve this:
https://blog.jetbrains.com/kotlin/2021/09/kotlinx-serialization-1-3-released/#excluding-nulls

You can ignore all defaults and do something like this:
#Serializable
data class Event(
#SerialName("temperature") val temperature: Float?,
#SerialName("pressure") val pressure: Float? = null,
#SerialName("humidity") val humidity: Float?,
)
val jsonMapper = Json { encodeDefaults = false}
val body = jsonMapper.encodeToString(Event(temperature = 42.0f,pressure = null, humidity = 20.9f))
Please, be aware of that, in the above case you are ignoring ALL defaults.
If you want to ignore only null values you have to implement a custom serializer.
For this example custom serializer will look like this:
object EventSerializer: KSerializer<Event> {
override fun deserialize(decoder: Decoder): Event {
decoder.decodeStructure(descriptor) {
var temperature: Float? = null
var humidity:Float? = null
var pressure: Float? = null
while (true) {
when (val index = decodeElementIndex(descriptor)) {
0 -> temperature = decodeFloatElement(descriptor, 0)
1 -> pressure = decodeFloatElement(descriptor, 1)
2 -> humidity = decodeFloatElement(descriptor, 2)
CompositeDecoder.DECODE_DONE -> break
else -> error("Unexpected index: $index")
}
}
return Event(temperature, pressure, humidity)
}
}
override fun serialize(encoder: Encoder, value: Event) {
encoder.beginStructure(descriptor).run {
value.temperature?.let { encodeStringElement(descriptor, 0, it.toString()) }
value.pressure?.let { encodeStringElement(descriptor, 1, it.toString()) }
value.humidity?.let { encodeStringElement(descriptor, 2, it.toString()) }
endStructure(descriptor)
}
}
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Event") {
element<String>("temperature")
element<String>("pressure")
element<String>("humidity")
}
}
To use it -> #Serializable(with = EventSerializer::class)

Related

Jetpack Paging 3.0 with Airbnb/epoxy only load the first page

I am trying to use Jetpack Paging 3.0 to get all the characters in Rick and Morty API using retrofit and display it using Airbnb/epoxy. But I only get the first page I've spent several hours trying to look for a solution but no luck.
Link to API
(Rick and Morty API)
CODE:
1 Retrofit
#GET("character/")
suspend fun getAllCharactersByPage(
#Query("page") pageIndex : Int
): Response<GetAllCharactersByPageResponse>
[2] Paging Source
class AllCharacterPagingSource(
private val repository: AllCharactersRepository) : PagingSource<Int, Character>() {
override suspend fun load(
params: LoadParams<Int>
): LoadResult<Int, Character> {
val pageNumber = params.key ?: 1
val prevKey = if (pageNumber == 1) null else pageNumber + 1
val response = NetworkLayer.apiClient.getAllCharactersByPage(pageNumber)
response.getException?.let {
return LoadResult.Error(it)
}
return LoadResult.Page(
data = response.body.results.map { CharacterMapper.buildFrom(it) },
prevKey = prevKey,
nextKey = getPageIndexFromNext(response.body.info.next)
)
}
override fun getRefreshKey(state: PagingState<Int, Character>): Int? {
return state.anchorPosition?.let {
state.closestPageToPosition(it)?.prevKey?.plus(1)
?: state.closestPageToPosition(it)?.nextKey?.minus(1)
}
}
private fun getPageIndexFromNext(next: String?): Int?{
return next?.split("?page=")?.get(1)?.toInt()
}
}
[3] View Model (Pager)
class AllCharactersViewModel : ViewModel() {
private val repository = AllCharactersRepository()
val flow = Pager(
PagingConfig(Constants.PAGE_SIZE, Constants.PREFETCH_SIZE, enablePlaceholders = false)
) {
AllCharacterPagingSource(repository)
}.flow.cachedIn(viewModelScope)
}
[4] Fragment (Submitting data)
iewLifecycleOwner.lifecycleScope.launch {
allCharactersViewModel.flow.collectLatest {
pagingEpoxyController.submitData(it)
}
}
binding.charactersEpoxyRecyclerView.setController(pagingEpoxyController)
[5] Epoxy Controller
class CharactersPagingEpoxyController : PagingDataEpoxyController<Character>() {
var context : Context? = null
override fun buildItemModel(currentPosition: Int, item: Character?): EpoxyModel<*> {
return CharacterCardModel(
character = item!!,
context = context!!,
onClick = { characterId ->
}
).id("characters_${item.id}")
}
data class CharacterCardModel(
val character : Character,
val context : Context,
val onClick: (Int) -> Unit
) : ViewBindingKotlinModel<CharacterCardContainerModelBinding>(R.layout.character_card_container_model) {
override fun CharacterCardContainerModelBinding.bind() {
Glide.with(context)
.load(character.image)
.into(imageView)
characterName.text = character.name
mirroredCharacterName.text = character.name
}
}
}
Thanks in advance!!!
I guess prevKey should be something like this
val previewPage= if (pageNumber ==1 ) null else pageNumber -1

How can I improve the verbose code when I use enum in Kotlin?

The are many items in a enum class , I found there ae many verbose code in the function fun getLevel.
How can I improve it?
Code A
enum class ELevel(private val labelId: Int, val alarmed: Boolean){
Normal(R.string.Normal,false),
Leaves(R.string.Leaves,false),
Whispering(R.string.Whispering,false),
Quiet(R.string.Quiet,false),
Refrigerator(R.string.Refrigerator,false),
Electric(R.string.Electric,false),
Washing(R.string.Washing,true),
Alarm(R.string.Alarm,true),
Subway(R.string.Subway ,true),
Factory(R.string.Factory,true),
Car(R.string.Car,true),
Ambulance(R.string.Ambulance,true);
fun getLabel(mContext: Context) = mContext.getString(labelId)
companion object {
fun getLevel(soundValue: Double): ELevel {
var temp = Normal
val i = soundValue.toInt()
if (i in 1..10) {
temp = Normal
}
if (i in 11..20) {
temp = Leaves
}
...
if (i in 101..110) {
temp = Car
}
if (i in 111..120) {
temp = Ambulance
}
return temp
}
}
In the same way that you have associates a labelId and alarmed flag with each of the enum constants, you can add an additional maxSoundLevel property:
enum class ELevel(
private val labelId: Int,
val alarmed: Boolean,
val maxSoundLevel: Int,
){
Normal(R.string.Normal,false, 10),
Leaves(R.string.Leaves,false, 20),
...
}
Then you can do:
companion object {
fun getLevel(soundValue: Double): ELevel =
// assuming the max sound levels are in ascending order
values().firstOrNull { soundValue.toInt() <= it.maxSoundLevel }
// if there is no match, throw exception. You can also just return the nullable ELevel
?: throw IllegalArgumentException("Unknown sound")
}
One way could be to create an abstract val range and define it for each enum. After that you could simply check and get the first enum that have your soundValue in his range.
enum class ELevel(private val labelId: Int, val alarmed: Boolean) {
Normal(R.string.Normal, false) {
override val range: IntRange = 1..10
},
Leaves(R.string.Leaves, false) {
override val range: IntRange = 11..20
},
Whispering(R.string.Whispering, false) {
override val range: IntRange = ...
},
Quiet(R.string.Quiet, false) {
override val range: IntRange = ...
},
Refrigerator(R.string.Refrigerator, false) {
override val range: IntRange = ...
},
Electric(R.string.Electric, false){
override val range: IntRange = ...
},
Washing(R.string.Washing, true){
override val range: IntRange = ...
},
Alarm(R.string.Alarm, true){
override val range: IntRange = ...
},
Subway(R.string.Subway, true){
override val range: IntRange = ...
},
Factory(R.string.Factory, true){
override val range: IntRange = ...
},
Car(R.string.Car, true) {
override val range: IntRange = 101..110
},
Ambulance(R.string.Ambulance, true) {
override val range: IntRange = 111..120
};
fun getLabel(mContext: Context) = mContext.getString(labelId)
abstract val range: IntRange
companion object {
fun getLevel(soundValue: Double): ELevel =
values().first { it.range.contains(soundValue.toInt()) }
}
}
If the IDs of the Strings are exact matches for the enum value names, you can also use identifier look-up to shorten your code a little bit more.
enum class ELevel(
val alarmed: Boolean,
private val maxSoundLevel: Int,
){
Normal(false, 10),
Leaves(false, 20),
//...
;
private var labelId = 0
fun getLabel(context: Context){
if (labelId == 0) {
labelId = context.resources.getIdentifier(name, "id", context.packageName)
}
return context.getString(labelId)
}
}

Data class toString name clash property / construction arg

Newby in Kotlin, I'm trying to implement a simple data class with constraint validation in fields.
This works great, but my solution is suboptimal since it exposes the private variables names defined in the class' definition in the toString representation, where I would prefer to have the properties:
data class MutablePointKt(private val _x: Int = 0, private val _y: Int = 0) {
private fun validatePositiveOrZero(some:Int) {
Validate.isTrue(some >= 0, "negative coordinate provided: $some")
}
var x: Int = 0
get() {
println(" > getting x: $field")
return field
}
set(value) {
validatePositiveOrZero(value)
field = value
}
var y: Int = 0
get() {
println(" > getting y: $field")
return field
}
set(value) {
validatePositiveOrZero(value)
field = value
}
init {
this.x = _x;
this.y = _y;
}
}
println(MutablePointKt(1, 2)) // prints "MutablePointKt(_x=1, _y=2)". how to print "MutablePointKt(x=1, y=2)" ?
Thank you !
EDIT:
I have a solution with
override fun toString(): String = ToStringBuilder.reflectionToString(this, KotlinToStringStyle()) and
class KotlinToStringStyle : ToStringStyle() {
private fun isFiltered(s: String?) = s?.startsWith("_") == true
override fun appendFieldStart(buffer: StringBuffer?, fieldName: String?) {
if (!isFiltered(fieldName))
super.appendFieldStart(buffer, fieldName)
}
override fun appendDetail(buffer: StringBuffer?, fieldName: String?, any: Any) {
if (!isFiltered(fieldName))
super.appendDetail(buffer, fieldName, any)
}
override fun appendFieldEnd(buffer: StringBuffer?, fieldName: String?) {
if (!isFiltered(fieldName))
super.appendFieldEnd(buffer, fieldName)
}
}
... but this is rather overkill, I would prefer a concise solution aka "the Kotlin way"

Kotlinx Serialization - Custom serializer to ignore null value

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.

Kotlin vert.x parsing a JSON String to a Data class using gson always returns null

I am just playing around with vert.x 3.5.3 Kotlin and I am unable to parse a JSON string into a Data class using gson.
Here is the code
class DataVerticle : AbstractVerticle() {
override fun start(startFuture: Future<Void>) {
data class Product(
#SerializedName("id") val id: Int,
#SerializedName("name") val name: String,
#SerializedName("productCode") val productCode: String
)
val products: MutableList<Product> = mutableListOf()
val gson = Gson()
val eventBus = vertx.eventBus()
eventBus.consumer<String>("data.verticle") {
when (it.headers().get("ACTION")) {
"ADD_PRODUCT" -> {
val prodJson = it.body()
if (prodJson != null) {
println(prodJson)
val product = gson.fromJson(prodJson, Product::class.java)
println(product)
it.reply("SUCCESS")
}
}
else -> {
print("ERROR")
}
}
}
startFuture.complete()
}
}
The Problem is the parsed value is always null
Here is my sample json ->
{"id":1,"name":"SOAP","productCode":"P101"}
The json string sent over the eventBus is not null.
I am using this package for gson
com.google.code.gson', version: '2.8.5'
Thanks
You declare your class inside the method body, which Gson doesn't like much.
Extracting it to be nested class will work just fine:
class DataVerticle : AbstractVerticle() {
override fun start(startFuture: Future) {
val gson = Gson()
val eventBus = vertx.eventBus()
eventBus.consumer<String>("data.verticle") {
when (it.headers().get("ACTION")) {
"ADD_PRODUCT" -> {
val prodJson = it.body()
if (prodJson != null) {
println(prodJson)
val product = gson.fromJson(prodJson, Product::class.java)
println(product)
it.reply("SUCCESS")
}
}
else -> {
print("ERROR")
}
}
}
startFuture.complete()
}
data class Product(
#SerializedName("id") val id: Int,
#SerializedName("name") val name: String,
#SerializedName("productCode") val productCode: String
)
}
Tested with:
val vertx = Vertx.vertx()
vertx.deployVerticle(DataVerticle()) {
val options = DeliveryOptions()
options.addHeader("ACTION", "ADD_PRODUCT")
vertx.eventBus().send("data.verticle", """{"id":1,"name":"SOAP","productCode":"P101"}""", options)
}