Search code examples
jsonkotlinserializationdeserializationcamelcasing

Kotlin - How to set Kotlinx.serialization Global Field Name Policy for different field cases


I'm trying to make every json key camel-case regardless of how it's formatted. Like:

  • ProjectName -> projectName

or

  • project_name -> projectName

with kotlin serialization

Things that do not work

  • I want to do this on a large scale so using @SerialName on every single parameter is not plausible, requires too much work and is error prone

  • Following returned json key naming strategy breaks kotlin code style guides and would be backward incompatible

Things I've tried

Example code

I looked at the docs and saw this example

However using namingStrategy on Json builder, won't work the way I wanted to

Although below code works to serialize class parameters to snake case

import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNamingStrategy

@OptIn(ExperimentalSerializationApi::class)
fun main() {
    @Serializable
    data class Project(val projectName: String, val projectOwner: String)

    val format = Json { namingStrategy = JsonNamingStrategy.SnakeCase }

    val project = format.decodeFromString<Project>("""{"project_name":"kotlinx.coroutines", "project_owner":"Kotlin"}""")
    println(format.encodeToString(project.copy(projectName = "kotlinx.serialization")))
// {"project_name":"kotlinx.serialization","project_owner":"Kotlin"}
}

Below code doesn't work to deserialize pascal case to snake case

import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNamingStrategy

@OptIn(ExperimentalSerializationApi::class)
fun main() {
    @Serializable
    data class Project(val project_name: String, val project_owner: String)

    val format = Json { namingStrategy = JsonNamingStrategy.SnakeCase }

    val project = format.decodeFromString<Project>("""{"ProjectName":"kotlinx.coroutines", "ProjectOwner":"Kotlin"}""")
    println(format.encodeToString(project.copy(project_name = "kotlinx.serialization")))
}

Running this code results in:

Exception in thread "main" kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 2: Encountered an unknown key 'ProjectName' at path: $
Use 'ignoreUnknownKeys = true' in 'Json {}' builder to ignore unknown keys.
JSON input: {"ProjectName":"kotlinx.coroutines", "ProjectOwner":"Kotlin"}
    at kotlinx.serialization.json.internal.JsonExceptionsKt.JsonDecodingException(JsonExceptions.kt:24)
    at kotlinx.serialization.json.internal.JsonExceptionsKt.JsonDecodingException(JsonExceptions.kt:32)
    at kotlinx.serialization.json.internal.AbstractJsonLexer.fail(AbstractJsonLexer.kt:584)
    at kotlinx.serialization.json.internal.AbstractJsonLexer.failOnUnknownKey(AbstractJsonLexer.kt:579)
    at kotlinx.serialization.json.internal.StreamingJsonDecoder.handleUnknown(StreamingJsonDecoder.kt:254)
    at kotlinx.serialization.json.internal.StreamingJsonDecoder.decodeObjectIndex(StreamingJsonDecoder.kt:240)
    at kotlinx.serialization.json.internal.StreamingJsonDecoder.decodeElementIndex(StreamingJsonDecoder.kt:175)
    at MainKt$main$Project$$serializer.deserialize(Main.kt:9)
    at MainKt$main$Project$$serializer.deserialize(Main.kt:9)
    at kotlinx.serialization.json.internal.StreamingJsonDecoder.decodeSerializableValue(StreamingJsonDecoder.kt:70)
    at kotlinx.serialization.json.Json.decodeFromString(Json.kt:107)
    at MainKt.main(Main.kt:17)
    at MainKt.main(Main.kt)

My case implementation

Implementing custom name strategy resulted in the same error above

Full code
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNamingStrategy
import kotlinx.serialization.descriptors.SerialDescriptor

@OptIn(ExperimentalSerializationApi::class)
public object CamelCase : JsonNamingStrategy {
    override fun serialNameForJson(descriptor: SerialDescriptor, elementIndex: Int, serialName: String): String {
        val words = serialName.split(Regex("[^a-zA-Z0-9]+"))
        return buildString {
            words.forEachIndexed { index, word ->
                if (index == 0) {
                    append(word.lowercase())
                } else {
                    append(word.replaceFirstChar(Char::titlecase))
                }
            }
        }
    }

    override fun toString(): String = "CamelCase"
}

@OptIn(ExperimentalSerializationApi::class)
public fun main() {
    @Serializable
    data class Project(val projectName: String, val projectOwner: String)

    val format = Json { namingStrategy = CamelCase }

    val project =
        format.decodeFromString<Project>("""{"project_name":"kotlinx.coroutines", "project_owner":"Kotlin"}""")
    println(format.encodeToString(project.copy(projectName = "kotlinx.serialization")))
}
Error output
Exception in thread "main" kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 2: Encountered an unknown key 'project_name' at path: $
Use 'ignoreUnknownKeys = true' in 'Json {}' builder to ignore unknown keys.
JSON input: {"project_name":"kotlinx.coroutines", "project_owner":"Kotlin"}
    at kotlinx.serialization.json.internal.JsonExceptionsKt.JsonDecodingException(JsonExceptions.kt:24)
    at kotlinx.serialization.json.internal.JsonExceptionsKt.JsonDecodingException(JsonExceptions.kt:32)
    at kotlinx.serialization.json.internal.AbstractJsonLexer.fail(AbstractJsonLexer.kt:584)
    at kotlinx.serialization.json.internal.AbstractJsonLexer.failOnUnknownKey(AbstractJsonLexer.kt:579)
    at kotlinx.serialization.json.internal.StreamingJsonDecoder.handleUnknown(StreamingJsonDecoder.kt:254)
    at kotlinx.serialization.json.internal.StreamingJsonDecoder.decodeObjectIndex(StreamingJsonDecoder.kt:240)
    at kotlinx.serialization.json.internal.StreamingJsonDecoder.decodeElementIndex(StreamingJsonDecoder.kt:175)
    at MainKt$main$Project$$serializer.deserialize(Main.kt:28)
    at MainKt$main$Project$$serializer.deserialize(Main.kt:28)
    at kotlinx.serialization.json.internal.StreamingJsonDecoder.decodeSerializableValue(StreamingJsonDecoder.kt:70)
    at kotlinx.serialization.json.Json.decodeFromString(Json.kt:107)
    at MainKt.main(Main.kt:37)
    at MainKt.main(Main.kt)

Solution

  • Apparently I was using it wrong. There's no strategy that can parse both project_name and ProjectName. For that, it's better to use @JsonNames