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

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)
}

Related

Kotlin - Ktor - How to handle Optional API resource fields in PATCH calls?

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

Spring webtestclient serializes dates to timestamps instead of dates

I am trying to check if the data I get back from the webtestclient is the same as what I expect. But the ZonedDateTime from the User data class is not just shown as a date but as a timestamp while I have applied Jackson to the webtestclient codecs. Example: 2021-12-09T16:39:43.225207700+01:00 is converted to 1639064383.225207700 while I expect nothing to change. Could someone maybe explain what I am doing wrong. (Using this jackson config when calling this endpoint outside of the test gives the date not as timestamp)
WebTestClientUtil:
object WebTestClientUtil {
fun webTestClient(routerFunction: RouterFunction<ServerResponse>): WebTestClient {
return WebTestClient
.bindToRouterFunction(routerFunction)
.configureClient()
.codecs { configurer: ClientCodecConfigurer ->
configurer.defaultCodecs().jackson2JsonEncoder(Jackson2JsonEncoder(objectMapper, MediaType.APPLICATION_JSON))
configurer.defaultCodecs().jackson2JsonDecoder(Jackson2JsonDecoder(objectMapper, MediaType.APPLICATION_JSON))
}
.build()
}
}
Testcase:
#Test
fun `get user when given correct data`() {
val user = GlobalMocks.mockedUser
coEvery { userRepository.getUserWithData(any()) } returns user
val result = webTestClient.get()
.uri("/api/v1/user/${user.userId}")
.exchange()
.expectStatus().is2xxSuccessful
.expectBody<Result>().returnResult().responseBody?.payload
assertEquals(user, result)
}
data class Result(
val payload: User
)
Jackson config:
class JacksonConfig {
companion object {
val serializationDateFormat: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXX")
val deserializationDateFormat: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm[:ss][XXX][X]")
val objectMapper = jacksonObjectMapper().applyDefaultSettings()
private fun ObjectMapper.applyDefaultSettings() =
apply {
disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)
disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE)
setSerializationInclusion(JsonInclude.Include.NON_NULL)
registerModule(Jdk8Module())
registerModule(ParameterNamesModule())
registerModule(JsonComponentModule())
registerModule(
JavaTimeModule().apply {
addSerializer(ZonedDateTime::class.java, ZonedDateTimeSerializer(serializationDateFormat))
addDeserializer(ZonedDateTime::class.java, ZonedDateTimeDeserializer())
}
)
}
}
class ZonedDateTimeDeserializer : JsonDeserializer<ZonedDateTime>() {
override fun deserialize(jsonParser: JsonParser, deserializationContext: DeserializationContext): ZonedDateTime {
val epochTime = jsonParser.text.toLongOrNull()
return if (epochTime != null) {
ZonedDateTime.ofInstant(
Instant.ofEpochSecond(epochTime),
currentZone
)
} else {
ZonedDateTime.parse(jsonParser.text, deserializationDateFormat)
}
}
}
}
EDIT: Also found this issue which makes me think that it might have something to do with bindToRouterFunction.
You need to define an ObjectMapper bean so that the auto-configured one is not used:
#Configuration(proxyBeanMethods = false)
class JacksonConfiguration {
companion object {
val serializationDateFormat: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXX")
val deserializationDateFormat: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm[:ss][XXX][X]")
}
#Bean
fun objectMapper() = jacksonObjectMapper().applyDefaultSettings ()
private fun ObjectMapper.applyDefaultSettings() =
apply {
disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)
disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE)
setSerializationInclusion(JsonInclude.Include.NON_NULL)
registerModule(Jdk8Module())
registerModule(ParameterNamesModule())
registerModule(JsonComponentModule())
registerModule(
JavaTimeModule().apply {
addSerializer(ZonedDateTime::class.java, ZonedDateTimeSerializer(serializationDateFormat))
addDeserializer(ZonedDateTime::class.java, ZonedDateTimeDeserializer())
}
)
}
class ZonedDateTimeDeserializer : JsonDeserializer<ZonedDateTime>() {
override fun deserialize(jsonParser: JsonParser, deserializationContext: DeserializationContext): ZonedDateTime {
val epochTime = jsonParser.text.toLongOrNull()
return if (epochTime != null) {
ZonedDateTime.ofInstant(
Instant.ofEpochSecond(epochTime),
currentZone
)
} else {
ZonedDateTime.parse(jsonParser.text, deserializationDateFormat)
}
}
}
}

How to serialize/deserialize json with nested field in kotlin?

I am using Json.decodeFromString<User>("json string") (https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serializers.md)
model is like
data class User(val id: String, val name: String, val assets: List<Asset>)
data class Asset(val id: String, val sku: String, val name: String)
but input json is like
{
"data": {
"id": "userId",
"name": "userName",
"body": {
"assets": [
{
"data": {
"id": "assetId",
"sku": "assetSku",
"name": "assetName"
}
}
]
}
}
}
How can I parse json with serializer? Seems not able to parse with delegate and surrogate serializers easily.
Your POJO seems to be wrong , every field needs to have name corresponding to json value , or use GSON's SerializedName annotation and Parse sing Gson.
Your User POJO should look something like this,
data class User (
#SerializedName("data") var data : UserData
)
data class UserData(
#SerializedName("id") var id : String,
#SerializedName("name") var name : String,
#SerializedName("body") var body : Body
)
data class Body (
#SerializedName("assets") var assets : List<Assets>
)
data class Assets (
#SerializedName("data") var data : AssetsData
)
data class AssetsData(
#SerializedName("id") var id : String,
#SerializedName("sku") var sku : String,
#SerializedName("name") var name : String
)
for serializing and deserializing
dependencies {
implementation 'com.google.code.gson:gson:2.8.9'
}
val gson = Gson()
val jsonValue = gson.toJson(User)
val jsonToUser = gson.fromJson(jsonValue ,User::class.java)
Read about https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/json.md#under-the-hood-experimental
Tried something like this:
#Serializable(with = AssetSerializer::class)
data class Asset(val id: String, val sku: String, val name: String)
#Serializable(with = UserSerializer::class)
data class User(val id: String, val name: String, val assets: List<Asset>)
object AssetSerializer: KSerializer<Asset> {
override val descriptor: SerialDescriptor =
buildClassSerialDescriptor("Asset") {
element("data", buildClassSerialDescriptor("data") {
element("id", String.serializer().descriptor)
element("sku", String.serializer().descriptor)
element("name", String.serializer().descriptor)
})
}
override fun serialize(encoder: Encoder, value: Asset) {
require(encoder is JsonEncoder)
encoder.encodeJsonElement(buildJsonObject {
put("data", buildJsonObject {
put("id", value.id)
put("sku", value.sku)
put("name", value.name)
})
})
}
override fun deserialize(decoder: Decoder): Asset {
require(decoder is JsonDecoder)
val root = decoder.decodeJsonElement()
val element = root.jsonObject["data"]!!
return Asset(
id = element.jsonObject["id"]!!.jsonPrimitive.content,
sku = element.jsonObject["sku"]!!.jsonPrimitive.content,
name = element.jsonObject["name"]!!.jsonPrimitive.content,
)
}
}
object UserSerializer: KSerializer<User> {
override val descriptor: SerialDescriptor =
buildClassSerialDescriptor("User") {
element("data", buildClassSerialDescriptor("data") {
element("id", String.serializer().descriptor)
element("name", String.serializer().descriptor)
element("body", buildClassSerialDescriptor("body") {
element("assets", ListSerializer(Asset.serializer()).descriptor)
})
})
}
override fun serialize(encoder: Encoder, value: User) {
require(encoder is JsonEncoder)
encoder.encodeJsonElement(buildJsonObject {
put("data", buildJsonObject {
put("id", value.id)
put("name", value.name)
put("body", buildJsonObject {
put("assets", JsonArray(value.assets.map { asset ->
encoder.json.encodeToJsonElement(asset)
}))
})
})
})
}
override fun deserialize(decoder: Decoder): User {
require(decoder is JsonDecoder)
val root = decoder.decodeJsonElement()
val element = root.jsonObject["data"]!!
val assets = element
.jsonObject["body"]!!
.jsonObject["assets"]!!
.jsonArray
.map { asset ->
decoder.json.decodeFromJsonElement(asset)
}
return Asset(
id = element.jsonObject["id"]!!.jsonPrimitive.content,
name = element.jsonObject["name"]!!.jsonPrimitive.content,
assets = assets,
)
}
}

Not able to return a desired value inside a overidden method using coroutines in kotlin

I am new to kotlin and coroutines.I have been working on a client-server part of an android app.I am using mediasoup-client-android library for this.
I am trying to intialize the sendtransports using createSendTransport() method.This method does have a sendTransportListener which has abstract methods.One of them being is onProduce() which returns a producerId which is a String. awaitEmit() is an asynchronous action and I need this in onProduce().To use awaitEmit() I used coroutines.But I need String as return type instead of Deferred.Is there any other way to implement the mentioned logic? Below is my code
class RoomClient {
suspend fun initTransports(device: Device) {
coroutineScope {
val id: String?
val iceParameters: String?
val iceCandidates: String?
val dtlsParameters: String?
val sctpParameters: String?
try {
val params = JSONObject()
params.put("forceTcp",false)
params.put("rtpCapabilities", this#RoomClient.device?.rtpCapabilities)
val res = socket?.awaitEmit("createWebRtcTransport",params)
val data = res?.get(0) as JSONObject
if (data.has("error")) {
Log.d(TAG, data.getString("error"))
return#coroutineScope
}
id = data.optString("id")
iceParameters = data.optString("iceParameters")
iceCandidates = data.optString("iceCandidates")
dtlsParameters = data.optString("dtlsParameters")
sctpParameters = data.optString("sctpParameters")
} catch (e: Throwable) {
Log.e(TAG, "${e.message}")
return#coroutineScope
}
val sendTransportListener: SendTransport.Listener = object : SendTransport.Listener {
private val listenerTAG = TAG.toString() + "_ProducerTrans"
override fun onProduce(
transport: Transport,
kind: String?,
rtpParameters: String?,
appData: String?
): String? {
this#coroutineScope.async{
var producerId: String? = null
Log.d(listenerTAG, "onProduce() ")
val producerDeferred = launch {
val params = JSONObject("""{"producerTransportId": transport.id, "kind": kind, "rtpParameters": rtpParameters,"appData": appData}""")
val res = socket?.awaitEmit("produce",params)
val data = res?.get(0) as JSONObject
producerId = data.getString("producer_Id")
}
producerDeferred.join()
Log.d(listenerTAG, "producerId inside the coroutine: $producerId"
}
return#async producerId
}
}
this#RoomClient.producerTransport = device.createSendTransport(
sendTransportListener, id,
iceParameters,
iceCandidates,
dtlsParameters
)
}
}
}
And also I am not sure about the way coroutines are used here.Please correct me If I have missed something central.

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.