Kotlinx.serialization part3

Peter Nagy
3 min readJan 27, 2021

This series is about Kotlinx serialization library.

In the Part1 I wrote about the basic usage of the Kotlinx.serialization.

In the Part2 I wrote about how can we configure the Kotlinx.serilaization.

Now I would like to show a real use case with some custom serialization:

Let's see an example: We have an object with some complex objects. Most times we are using Strings and primitives in our data classes. However sometimes we need to use some extra objects, like Date or Decimal objects. In this example we have a Warranty object. We want to show warranty information so there are 4 fields: name of the device, time of buying device, until we have warranty, and price of device.

data class(val nameOfDevice: String, val dateOfBuy: Date, val timeOfWarranty: Date, val price: BigDecimal)// and JSON{"nameOfDevice":"foo","dateOfBuy":"2021-01-27 22:26:25.919+0100","timeOfWarranty":"1643318785917","price":"12.345678912"}

I have written there are two Date field however in the JSON there is only one Date field, other is a Long, why ? Because when we bought a device it is an exact date (or time), and warranty is typically a time period, like 1 year. It is better to describe it with a timestamp or epoch instead of an exact date. The reason is a Date contains a time zone. When we want to show an exact date it will contains timezone information. (If there is time information like 12:34 without any timezone information then everybody is thinking in local time and it could be problem when we need calculate some exact time period) Time period, until our warranty is valid will be a Date like 2nd Dec 2021 midnight -> 02.12.2021 12:00PM of course it could have a timezone information too. It must be in same timezone like when warranty is beginning. :)

When we want to serialize or desearialize we will not able to do because there is no any serializer for Date and BigDecimal objects. So we need to write one.

object DateAsStringSerializer : KSerializer<Date> {
private val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSZ")

override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("date", PrimitiveKind.STRING)

override fun serialize(encoder: Encoder, value: Date) {
val string = formatter.format(value)
encoder.encodeString(string)
}

override fun deserialize(decoder: Decoder): Date {
val string = decoder.decodeString()
return formatter.parse(string)
}
}

This serializer will create a String value from Date object. As you can see date formatter is using ISO format and there is Z as timezone information. (It could be better to use GMT or UTC format instead of location based time zone).

object DateAsTimeStampSerializer : KSerializer<Date> {

override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("date", PrimitiveKind.LONG)

override fun serialize(encoder: Encoder, value: Date) {
encoder.encodeString(value.time.toString())
}

override fun deserialize(decoder: Decoder): Date {
val string = decoder.decodeString()
return Date(string.toLong())
}
}

This serializer will create a timestamp from the Date. As you can see it will not contains timezone information. :( But it is a calculated field maybe we can trust and convert to local time when we want to display it. (Timestamp will be an exact date time value of course)

object DecimalAsStringSerializer : KSerializer<BigDecimal> {

override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("decimal", PrimitiveKind.STRING)

override fun serialize(encoder: Encoder, value: BigDecimal) {
encoder.encodeString(value.toPlainString())
}

override fun deserialize(decoder: Decoder): BigDecimal {
val string = decoder.decodeString()
return BigDecimal(string)
}
}

And this serializer will create a String value from BigDecimal Object. It could be better to use a DecimalFormatter because decimal separator is local dependent thing (in some local "." is a decimal separator and "," is thousand separator and in other local "," is a decimal separator) and could cause some problem. Of course we need a contract how our backend will handle this information.

Ok, and how can we set those three custom serializer ?

@Serializable
data class Warranty(
val nameOfDevice: String,
@Serializable(with = DateAsStringSerializer::class) val dateOfBuy: Date,
@Serializable(with = DateAsTimeStampSerializer::class) val timeOfWarranty: Date,
@Serializable(with = DecimalAsStringSerializer::class) val price: BigDecimal
)

We can annotate a field with a custom serializer.

And check it:

Json.decodeFromString<Warranty>(jsonstring)// Warranty object:
Warranty(nameOfDevice=foo, dateOfBuy=Wed Jan 27 22:26:25 CET 2021, timeOfWarranty=Thu Jan 27 22:26:25 CET 2022, price=12.345678912)

Thanks for reading my article, if you found this post useful? Kindly tap the 👏 button below and follow :) .

In the next part I will write how can we use custom serializer when we want serialize a 3rd party class.

--

--