kotlinx serializer decode snake case to camel case without serial name

482 views Asked by At

Question

Hi I'm new to kotlinx serialization. the json is snake case, and kotlin data class uses camel case. is there a way to parse snake case by using custom serilizer and deserializer?

I know how to do...

val format = Json { namingStrategy = JsonNamingStrategy.SnakeCase }

val project = format.decodeFromString<Project>("""{"project_name":"kotlinx.coroutines", "project_owner":"Kotlin"}""")
assertThat(project.projectName).isEqualTo("kotlinx.coroutines")
assertThat(project.projectOwner).isEqualTo("Kotlin")

but I want to override deserializer and place this logic inside of companion object. is this possible?

My code

import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encodeToString
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.encoding.decodeStructure
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNamingStrategy

@kotlinx.serialization.ExperimentalSerializationApi
private val json = Json { namingStrategy = JsonNamingStrategy.SnakeCase }

@Serializable(PaymentInfo.Companion::class)
data class PaymentInfo(
    val paymentNo: String,
    val paymentDate: String,
    val paymentService: String,
    
    ) {
    companion object: KSerializer<PaymentInfo> {
        // what descriptor should I use?
        override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("PaymentInfo", PrimitiveKind.)

        override fun serialize(encoder: Encoder, value: PaymentInfo) {
            // what to do ?
        }
        override fun deserialize(decoder: Decoder): PaymentInfo {
            // what to do ?
        }
    }
}

Edited

I want to avoid adding @SerialName to every member!

1

There are 1 answers

1
Slaw On BEST ANSWER

@SerialName

First, I just want to mention that I don't understand your wish to avoid @SerialName. You mentioned:

I'm afraid that that's too much labor.

But as you'll see in the next section of this answer, the custom serializer is quite a bit of code. And the same thing can be accomplished with just:

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class PaymentInfo(
    @SerialName("payment_no") val paymentNo: String,
    @SerialName("payment_date") val paymentDate: String,
    @SerialName("payment_service") val paymentService: String
)

For something as simple as changing the serial name of a property, I strongly recommend you use @SerialName instead of writing a completely custom serializer.


Custom Serializer

If you really want to avoid @SerialName, then you can customize the serialized names via a custom serializer. If you have:

import kotlinx.serialization.Serializable

@Serializable(PaymentInfoSerializer::class)
data class PaymentInfo(
    val paymentNo: String,
    val paymentDate: String,
    val paymentService: String
)

And you want the following custom names:

Class property Serial Name
paymentNo payment_no
paymentDate payment_date
paymentService payment_service

Then the custom PaymentInfoSerializer might look like this:

import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.descriptors.element
import kotlinx.serialization.encoding.*

// You can move this to the companion object of PaymentInfo if
// you really want to.
object PaymentInfoSerializer : KSerializer<PaymentInfo> {

    override val descriptor = buildClassSerialDescriptor("PaymentInfo") {
        element<String>("payment_no")
        element<String>("payment_date")
        element<String>("payment_service")
    }

    override fun serialize(encoder: Encoder, value: PaymentInfo) {
        encoder.encodeStructure(descriptor) {
            encodeStringElement(descriptor, 0, value.paymentNo)
            encodeStringElement(descriptor, 1, value.paymentDate)
            encodeStringElement(descriptor, 2, value.paymentService)
        }
    }

    override fun deserialize(decoder: Decoder): PaymentInfo {
        return decoder.decodeStructure(descriptor) {
            var paymentNo = ""
            var paymentDate = ""
            var paymentService = ""
            while (true) {
                when (val index = decodeElementIndex(descriptor)) {
                    0 -> paymentNo = decoder.decodeString()
                    1 -> paymentDate = decoder.decodeString()
                    2 -> paymentService = decoder.decodeString()
                    CompositeDecoder.DECODE_DONE -> break
                    else -> error("Unknown index: $index")
                }
            }
            PaymentInfo(paymentNo, paymentDate, paymentService)
        }
    }
}

See the Serializers chapter of the Kotlin Serialization Guide for more information.

Here is the above serializer in use:

import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

fun main() {
    val original = PaymentInfo("42", "2023-11-12", "foo")
    val encoded = Json.encodeToString(original)
    val decoded = Json.decodeFromString<PaymentInfo>(encoded)
    println(original)
    println(encoded)
    println(decoded)
}

Which gives the following output:

PaymentInfo(paymentNo=42, paymentDate=2023-11-12, paymentService=foo)
{"payment_no":"42","payment_date":"2023-11-12","payment_service":"foo"}
PaymentInfo(paymentNo=42, paymentDate=2023-11-12, paymentService=foo)