Kotlin Help

Transform JSON structure

To control the structure and content of the JSON generated during serialization, you can create custom serializers. For smaller adjustments, such as wrapping values or unwrapping arrays, the JsonTransformingSerializer class provides a simpler way to modify JSON by working directly with the JSON element tree instead of using the Encoder or Decoder manually.

The JsonTransformingSerializer is a JSON-specific abstract serializer that implements the KSerializer interface. It provides the transformSerialize() and transformDeserialize() functions that you can override to adjust the JSON element tree before serialization or deserialization.

In addition to transforming JSON structures, you can also use JsonContentPolymorphicSerializer to select the appropriate polymorphic class based on the JSON content.

Modify JSON structure

You can adjust the JSON structure by transforming the JSON element tree. The following examples demonstrate common use cases, such as wrapping or unwrapping arrays and omitting specific properties.

Wrap a single object in an array during deserialization

Some APIs return a single JSON object for one item and a JSON array for multiple items. To deserialize both cases into a List:

  1. Create a subclass of JsonTransformingSerializer and specify a serializer in its constructor. To use the standard conversion logic, pass the default serializer for the target type, such as ListSerializer() for lists.

  2. Override the transformDeserialize() function.

Here's an example:

// Imports declarations from the serialization library import kotlinx.serialization.* import kotlinx.serialization.builtins.* import kotlinx.serialization.json.* //sampleStart @Serializable data class Project( val name: String, // Specifies UserListSerializer to handle the serialization of the users property @Serializable(with = UserListSerializer::class) val users: List<User> ) @Serializable data class User(val name: String) // Creates a serializer that transforms the results of the default List serializer object UserListSerializer : JsonTransformingSerializer<List<User>>(ListSerializer(User.serializer())) { override fun transformDeserialize(element: JsonElement): JsonElement = // If the element is not a JsonArray, wraps it into a single-element array if (element !is JsonArray) JsonArray(listOf(element)) else element } fun main() { println(Json.decodeFromString<Project>(""" {"name":"kotlinx.serialization","users":{"name":"kotlin"}} """)) // Project(name=kotlinx.serialization, users=[User(name=kotlin)]) println(Json.decodeFromString<Project>(""" {"name":"kotlinx.serialization","users":[{"name":"kotlin"},{"name":"jetbrains"}]} """)) // Project(name=kotlinx.serialization, users=[User(name=kotlin), User(name=jetbrains)]) } //sampleEnd

Unwrap a single-element array during serialization

To unwrap a single-element list into a single JSON object during serialization, override the transformSerialize() function:

// Imports declarations from the serialization library import kotlinx.serialization.* import kotlinx.serialization.builtins.* import kotlinx.serialization.json.* //sampleStart @Serializable data class Project( val name: String, // Specifies UserListSerializer to handle serialization of the users property @Serializable(with = UserListSerializer::class) val users: List<User> ) @Serializable data class User(val name: String) // Creates a serializer that transforms the results of the default List serializer object UserListSerializer : JsonTransformingSerializer<List<User>>(ListSerializer(User.serializer())) { override fun transformSerialize(element: JsonElement): JsonElement { require(element is JsonArray) // Unwraps single-element lists into a single JSON object return element.singleOrNull() ?: element } } fun main() { val data = Project("kotlinx.serialization", listOf(User("kotlin"))) println(Json.encodeToString(data)) // {"name":"kotlinx.serialization","users":{"name":"kotlin"}} } //sampleEnd

Omit specific properties during serialization

If you can't specify a default value, but still want to omit a property when it has a specific value, use JsonTransformingSerializer:

  1. Create a subclass of JsonTransformingSerializer and specify a serializer in its constructor.

  2. Override the transformSerialize() function.

Here's an example where the Project class has a language property, which is omitted when its value is "Kotlin":

// Imports declarations from the serialization library import kotlinx.serialization.* import kotlinx.serialization.builtins.* import kotlinx.serialization.json.* //sampleStart @Serializable class Project(val name: String, val language: String) // Creates a custom serializer that omits the language property if it's equal to "Kotlin" object ProjectSerializer : JsonTransformingSerializer<Project>(Project.serializer()) { override fun transformSerialize(element: JsonElement): JsonElement = // Omits the language property if its value is "Kotlin" JsonObject(element.jsonObject.filterNot { (k, v) -> k == "language" && v.jsonPrimitive.content == "Kotlin" }) } fun main() { val data = Project("kotlinx.serialization", "Kotlin") // Uses the default serializer println(Json.encodeToString(data)) // {"name":"kotlinx.serialization","language":"Kotlin"} // Applies the custom serializer to omit the language property println(Json.encodeToString(ProjectSerializer, data)) // {"name":"kotlinx.serialization"} } //sampleEnd

Select the appropriate polymorphic class based on the JSON content

In polymorphic serialization, JSON often contains a dedicated class discriminator property that identifies the concrete subtype during deserialization.

If the JSON input doesn't contain a class discriminator, you can use JsonContentPolymorphicSerializer to infer the type from the structure of the JSON. This serializer lets you override the selectDeserializer() function to choose the correct deserializer based on the JSON content.

Here's an example where all values share a common base type with a name property:

@Serializable abstract class Project { abstract val name: String } @Serializable data class BasicProject(override val name: String): Project() @Serializable data class OwnedProject(override val name: String, val owner: String) : Project()

To distinguish between BasicProject and OwnedProject, override the selectDeserializer() function. You can use this function to check whether the JSON object contains the owner key, and return the corresponding serializer:

// Creates a custom serializer that selects deserializer based on the presence of "owner" object ProjectSerializer : JsonContentPolymorphicSerializer<Project>(Project::class) { override fun selectDeserializer(element: JsonElement) = when { // Selects the OwnedProject serializer if the JSON object contains an "owner" key "owner" in element.jsonObject -> OwnedProject.serializer() else -> BasicProject.serializer() } }

When you use this serializer to serialize data, Kotlin serialization uses the serializer of the value's actual runtime type. This could be the serializer specified in a SerializersModule or the default serializer:

// Imports declarations from the serialization library import kotlinx.serialization.* import kotlinx.serialization.builtins.* import kotlinx.serialization.json.* @Serializable abstract class Project { abstract val name: String } @Serializable data class BasicProject(override val name: String): Project() @Serializable data class OwnedProject(override val name: String, val owner: String) : Project() // Creates a custom serializer that selects deserializer based on the presence of "owner" object ProjectSerializer : JsonContentPolymorphicSerializer<Project>(Project::class) { override fun selectDeserializer(element: JsonElement) = when { // Selects the OwnedProject serializer if the JSON object contains an "owner" key "owner" in element.jsonObject -> OwnedProject.serializer() else -> BasicProject.serializer() } } //sampleStart fun main() { val data = listOf( OwnedProject("kotlinx.serialization", "kotlin"), BasicProject("example") ) // No class discriminator in the JSON output val string = Json.encodeToString(ListSerializer(ProjectSerializer), data) println(string) // [{"name":"kotlinx.serialization","owner":"kotlin"},{"name":"example"}] println(Json.decodeFromString(ListSerializer(ProjectSerializer), string)) // [OwnedProject(name=kotlinx.serialization, owner=kotlin), BasicProject(name=example)] } //sampleEnd

Add custom behavior to the default serializer

You can add custom behavior to the default serializer that Kotlin serialization generates, by using the default serializer as a delegate.

To do so, annotate a serializable class with the Experimental @KeepGeneratedSerializer and use the automatically generated generatedSerializer() as the base serializer in your custom JsonTransformingSerializer.

Here's an example that updates the JSON structure during deserialization by combining multiple input properties into a single name property expected by the target class:

// Imports declarations from the serialization library import kotlinx.serialization.* import kotlinx.serialization.json.* //sampleStart @OptIn(ExperimentalSerializationApi::class) @KeepGeneratedSerializer @Serializable(with = UserNameSerializer::class) // Defines a type with a name property data class User(val name: String) // Adds custom logic to the default serializer to combine input properties during deserialization object UserNameSerializer : JsonTransformingSerializer<User>(User.generatedSerializer()) { override fun transformDeserialize(element: JsonElement): JsonElement { val jsonObject = element.jsonObject val first = jsonObject["firstName"]?.jsonPrimitive?.content val last = jsonObject["lastName"]?.jsonPrimitive?.content // Combines input properties into the name property // if the input doesn't match the expected structure return if (first != null && last != null) { JsonObject(mapOf("name" to JsonPrimitive("$first $last"))) } else { jsonObject } } } fun main() { // Deserializes JSON where the name property is split across multiple input properties val fromExternalData = Json.decodeFromString<User>( """{"firstName":"John","lastName":"Smith"}""" ) println(fromExternalData) // User(name=John Smith) // Deserializes JSON where the name property matches the expected structure val fromInternalData = Json.decodeFromString<User>( """{"name":"John Smith"}""" ) println(fromInternalData) // User(name=John Smith) } //sampleEnd

Implement custom serialization logic in JSON

If the transformation functions provided by JsonTransformingSerializer or JsonContentPolymorphicSerializer aren't enough, you can implement custom serialization logic by defining your own KSerializer class.

You can get full control over how values are serialized and deserialized by overriding the serialize() and deserialize() functions directly.

When you implement custom serialization logic for JSON, you can cast Encoder to JsonEncoder and Decoder to JsonDecoder to call the JSON-specific functions decodeJsonElement() and encodeToJsonElement(). These functions allow you to retrieve JSON elements from the value the decoder is currently handling, or insert JSON elements into it.

Both JsonDecoder and JsonEncoder expose a json property that gives access to the active Json instance, which controls how values are encoded and decoded. Through that instance, you can use encodeToJsonElement() and decodeFromJsonElement() to convert between JsonElement instances and Kotlin objects.

Using these APIs, you can implement two-stage conversions:

  • Decode the input into a JsonElement first and then convert that element into a Kotlin value.

  • Convert a Kotlin value into a JsonElement first and then encode that element with the encoder.

Let's look at an example of a custom KSerializer that fully controls how values of a Response type are encoded and decoded in JSON. This serializer encodes an Ok response directly as a JSON value and an Error response as a JSON object that contains the error message:

// Imports declarations from the serialization library import kotlinx.serialization.* import kotlinx.serialization.builtins.* import kotlinx.serialization.json.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* // Defines a sealed class for API responses @Serializable(with = ResponseSerializer::class) sealed class Response<out T> { data class Ok<out T>(val data: T) : Response<T>() data class Error(val message: String) : Response<Nothing>() } // Implements custom serialization logic for Response class ResponseSerializer<T>( private val dataSerializer: KSerializer<T> ) : KSerializer<Response<T>> { override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Response") { element("Ok", dataSerializer.descriptor) element("Error", buildClassSerialDescriptor("Error") { element<String>("message") }) } // Deserializes a Response value from JSON override fun deserialize(decoder: Decoder): Response<T> { // Ensures that the decoder is a JsonDecoder require(decoder is JsonDecoder) // Decodes the input into a JsonElement val element = decoder.decodeJsonElement() // Converts the JsonElement into the corresponding Response value return if (element is JsonObject && "error" in element) { Response.Error(element["error"]!!.jsonPrimitive.content) } else { Response.Ok( decoder.json.decodeFromJsonElement(dataSerializer, element) ) } } // Serializes a Response value to JSON override fun serialize(encoder: Encoder, value: Response<T>) { // Ensures that the encoder is a JsonEncoder require(encoder is JsonEncoder) // Converts the Response value into a JsonElement val element = when (value) { is Response.Ok -> encoder.json.encodeToJsonElement(dataSerializer, value.data) is Response.Error -> buildJsonObject { put("error", value.message) } } // Encodes the JsonElement using the encoder encoder.encodeJsonElement(element) } } @Serializable data class Project(val name: String) fun main() { val responses = listOf( Response.Ok(Project("kotlinx.serialization")), Response.Error("Not found") ) val json = Json.encodeToString(responses) println(json) // [{"name":"kotlinx.serialization"},{"error":"Not found"}] println(Json.decodeFromString<List<Response<Project>>>(json)) // [Ok(data=Project(name=kotlinx.serialization)), Error(message=Not found)] }

Preserve unknown JSON attributes

A common use case for a custom JSON-specific serializer is preserving JSON properties from the input that your serializable class doesn't define. By default, these properties are ignored during deserialization.

To preserve these JSON properties, implement a custom JSON-specific serializer that collects all properties not defined in the target class into a dedicated JsonObject field during deserialization. This allows you to preserve these properties in the serializable class without modifying the original JSON structure.

Here's an example:

// Imports declarations from the serialization library import kotlinx.serialization.* import kotlinx.serialization.builtins.* import kotlinx.serialization.json.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* //sampleStart data class UnknownProject(val name: String, val details: JsonObject) object UnknownProjectSerializer : KSerializer<UnknownProject> { override val descriptor: SerialDescriptor = buildClassSerialDescriptor("UnknownProject") { element<String>("name") element<JsonElement>("details") } override fun deserialize(decoder: Decoder): UnknownProject { // Ensures the decoder is JSON-specific val jsonInput = decoder as? JsonDecoder ?: error("Can be deserialized only by JSON") // Reads the entire content as JSON val json = jsonInput.decodeJsonElement().jsonObject // Extracts and removes the name property val name = json.getValue("name").jsonPrimitive.content val details = json.toMutableMap() details.remove("name") return UnknownProject(name, JsonObject(details)) } override fun serialize(encoder: Encoder, value: UnknownProject) { error("Serialization is not supported") } } fun main() { // Deserializes JSON with properties not defined in the serializable class into UnknownProject println(Json.decodeFromString(UnknownProjectSerializer, """{"type":"unknown","name":"example","maintainer":"Unknown","license":"Apache 2.0"}""")) // UnknownProject(name=example, details={"type":"unknown","maintainer":"Unknown","license":"Apache 2.0"}) } //sampleEnd

In this example, the preserved JSON properties remain at the same level within the input JSON object as the properties defined in the serializable class.

What's next

10 June 2026