Skip to content

JsonTransformingSerializer with value classes and polymorphism #3039

@juriad

Description

@juriad

Describe the bug

The type discriminator field vanishes when I wrap the generated serializer with JsonTransformingSerializer.

I have a business description of operation that shall be performed. This operation is sent to multiple services through different protocols. Each of them requires some "transformation" of the operation. The wrapper determines which service to route the operation to. The wrappers are sometimes classes when no extra business attributes are needed, such as in the case below, where an additional field with version is added.

To Reproduce

import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.JsonTransformingSerializer
import kotlinx.serialization.json.jsonObject

@Serializable
sealed interface Operation

@Serializable
data class Insert(val id: Int, val name: String) : Operation

@Serializable
data class Delete(val id: Int) : Operation

@Serializable
sealed interface MessageWrapper

@JvmInline
@Serializable
value class InsertWrapper(val insert: Insert) : MessageWrapper

@JvmInline
@Serializable
value class DeleteWrapper(val delete: Delete) : MessageWrapper

object MessageWrapperSerializer : JsonTransformingSerializer<MessageWrapper>(MessageWrapper.serializer()) {
    override fun transformSerialize(element: JsonElement): JsonElement {
        return JsonObject(element.jsonObject + ("version" to JsonPrimitive("1.0")))
    }
}

fun main() {
    val json = Json { ignoreUnknownKeys = true }

    val message = InsertWrapper(Insert(1, "one"))

    val encoded = json.encodeToString(MessageWrapper.serializer(), message)
    println(encoded)

    val encoded2 = json.encodeToString(MessageWrapperSerializer, message)
    println(encoded2)
}

Prints:

{"type":"Insert","id":1,"name":"one"}
{"id":1,"name":"one","version":"1.0"}

Expected behavior

The type should be present in the second output too. I would expect it to read:

{"type":"Insert","id":1,"name":"one","version":"1.0"}

The example above works better when the wrapper class is not value class, but that changes the structure.

{"type":"InsertWrapper","insert":{"id":1,"name":"one"}}
{"type":"InsertWrapper","insert":{"id":1,"name":"one"},"version":"1.0"}

Environment

  • Kotlin version: 2.1.20
  • Library version: 1.8.1 (both
  • Kotlin platforms: JVM
  • Gradle version: 8.10.2
  • IDE version (if bug is related to the IDE) IntellijIDEA 2025.1.3
  • Other relevant context:

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions