CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-org-jetbrains-kotlinx--kotlinx-serialization-json-jvm

A Kotlin multiplatform library for JSON serialization providing type-safe JSON parsing and object serialization

Pending
Overview
Eval results
Files

advanced.mddocs/

Advanced Serialization

Advanced serialization interfaces, custom serializers, and low-level JSON processing APIs for specialized use cases requiring fine-grained control over serialization behavior.

Capabilities

JsonEncoder Interface

Low-level encoder interface for custom JSON serialization with direct JsonElement access.

/**
 * Encoder interface for JSON serialization with JsonElement support
 * Provides access to the Json instance and direct JsonElement encoding
 */
@SubclassOptInRequired(SealedSerializationApi::class)
interface JsonEncoder : Encoder, CompositeEncoder {
    /**
     * The Json instance used for encoding
     */
    val json: Json
    
    /**
     * Encode a JsonElement directly to the output
     * @param element JsonElement to encode
     */
    fun encodeJsonElement(element: JsonElement)
}

JsonDecoder Interface

Low-level decoder interface for custom JSON deserialization with direct JsonElement access.

/**
 * Decoder interface for JSON deserialization with JsonElement support
 * Provides access to the Json instance and direct JsonElement decoding
 */
@SubclassOptInRequired(SealedSerializationApi::class)
interface JsonDecoder : Decoder, CompositeDecoder {
    /**
     * The Json instance used for decoding
     */
    val json: Json
    
    /**
     * Decode the current JSON input as a JsonElement
     * @return JsonElement representing the current JSON value
     */
    fun decodeJsonElement(): JsonElement
}

Usage Examples:

import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlinx.serialization.encoding.*
import kotlinx.serialization.descriptors.*

// Custom serializer using JsonEncoder/JsonDecoder
object TimestampSerializer : KSerializer<Long> {
    override val descriptor = PrimitiveSerialDescriptor("Timestamp", PrimitiveKind.LONG)
    
    override fun serialize(encoder: Encoder, value: Long) {
        if (encoder is JsonEncoder) {
            // Custom JSON encoding - store as ISO string
            val isoString = java.time.Instant.ofEpochMilli(value).toString()
            encoder.encodeJsonElement(JsonPrimitive(isoString))
        } else {
            encoder.encodeLong(value)
        }
    }
    
    override fun deserialize(decoder: Decoder): Long {
        return if (decoder is JsonDecoder) {
            // Custom JSON decoding - parse ISO string
            val element = decoder.decodeJsonElement()
            if (element is JsonPrimitive && element.isString) {
                java.time.Instant.parse(element.content).toEpochMilli()
            } else {
                element.jsonPrimitive.long
            }
        } else {
            decoder.decodeLong()
        }
    }
}

@Serializable
data class LogEntry(
    val message: String,
    @Serializable(TimestampSerializer::class)
    val timestamp: Long
)

val entry = LogEntry("Application started", System.currentTimeMillis())
val json = Json.encodeToString(entry)
val decoded = Json.decodeFromString<LogEntry>(json)

JsonContentPolymorphicSerializer

Abstract base class for content-based polymorphic serialization where type selection is based on JSON content rather than discriminator fields.

/**
 * Abstract serializer for polymorphic serialization based on JSON content
 * Type selection is performed by examining the JsonElement structure
 */
abstract class JsonContentPolymorphicSerializer<T : Any>(
    baseClass: KClass<T>
) : KSerializer<T> {
    /**
     * Select the appropriate deserializer based on JSON content
     * @param element JsonElement representing the JSON to deserialize
     * @return DeserializationStrategy for the appropriate concrete type
     */
    abstract fun selectDeserializer(element: JsonElement): DeserializationStrategy<T>
}

Usage Examples:

import kotlinx.serialization.*
import kotlinx.serialization.json.*

@Serializable
sealed class ApiMessage

@Serializable
data class TextMessage(val text: String) : ApiMessage()

@Serializable
data class ImageMessage(val url: String, val width: Int, val height: Int) : ApiMessage()

@Serializable
data class LocationMessage(val latitude: Double, val longitude: Double, val address: String?) : ApiMessage()

// Content-based polymorphic serializer
object ApiMessageSerializer : JsonContentPolymorphicSerializer<ApiMessage>(ApiMessage::class) {
    override fun selectDeserializer(element: JsonElement): DeserializationStrategy<ApiMessage> {
        val jsonObject = element.jsonObject
        
        return when {
            "text" in jsonObject -> TextMessage.serializer()
            "url" in jsonObject && "width" in jsonObject -> ImageMessage.serializer()
            "latitude" in jsonObject && "longitude" in jsonObject -> LocationMessage.serializer()
            else -> throw SerializationException("Unknown message type")
        }
    }
}

// Register the serializer
val json = Json {
    serializersModule = SerializersModule {
        polymorphic(ApiMessage::class) {
            subclass(TextMessage::class)
            subclass(ImageMessage::class)
            subclass(LocationMessage::class)
        }
    }
}

// Usage - no discriminator field needed
val messages = listOf<ApiMessage>(
    TextMessage("Hello world"),
    ImageMessage("https://example.com/image.jpg", 800, 600),
    LocationMessage(40.7128, -74.0060, "New York, NY")
)

val encoded = json.encodeToString(messages)
// Result: [
//   {"text":"Hello world"},
//   {"url":"https://example.com/image.jpg","width":800,"height":600},
//   {"latitude":40.7128,"longitude":-74.0060,"address":"New York, NY"}
// ]

val decoded = json.decodeFromString<List<ApiMessage>>(encoded)

JsonTransformingSerializer

Abstract base class for transforming JsonElement during serialization and deserialization, allowing modification of JSON structure without changing the data class.

/**
 * Abstract serializer for transforming JsonElement during serialization/deserialization
 * Allows modification of JSON structure without changing the underlying data class
 */
abstract class JsonTransformingSerializer<T>(
    private val tSerializer: KSerializer<T>
) : KSerializer<T> {
    override val descriptor: SerialDescriptor = tSerializer.descriptor
    
    /**
     * Transform JsonElement after deserialization before converting to object
     * @param element JsonElement to transform
     * @return Transformed JsonElement
     */
    open fun transformDeserialize(element: JsonElement): JsonElement = element
    
    /**
     * Transform JsonElement after serialization from object
     * @param element JsonElement to transform  
     * @return Transformed JsonElement
     */
    open fun transformSerialize(element: JsonElement): JsonElement = element
}

Usage Examples:

import kotlinx.serialization.*
import kotlinx.serialization.json.*

@Serializable
data class UserData(
    val id: Int,
    val name: String,
    val email: String
)

// Transformer that adds/removes metadata fields
object UserDataTransformer : JsonTransformingSerializer<UserData>(UserData.serializer()) {
    
    override fun transformSerialize(element: JsonElement): JsonElement {
        val jsonObject = element.jsonObject
        return buildJsonObject {
            // Copy original fields
            jsonObject.forEach { (key, value) ->
                put(key, value)
            }
            // Add metadata
            put("version", "1.0")
            put("serializedAt", System.currentTimeMillis())
        }
    }
    
    override fun transformDeserialize(element: JsonElement): JsonElement {
        val jsonObject = element.jsonObject
        return buildJsonObject {
            // Copy only the fields we want, ignoring metadata
            put("id", jsonObject["id"]!!)
            put("name", jsonObject["name"]!!)
            put("email", jsonObject["email"]!!)
            // Ignore "version", "serializedAt", etc.
        }
    }
}

@Serializable(UserDataTransformer::class)
data class UserWithTransformer(
    val id: Int,
    val name: String, 
    val email: String
)

val user = UserWithTransformer(1, "Alice", "alice@example.com")
val json = Json.encodeToString(user)
// Result: {"id":1,"name":"Alice","email":"alice@example.com","version":"1.0","serializedAt":1234567890}

val decoded = Json.decodeFromString<UserWithTransformer>(json)
// Metadata fields are stripped during deserialization

Advanced Transformer Examples

Field Renaming Transformer:

import kotlinx.serialization.*
import kotlinx.serialization.json.*

@Serializable
data class InternalUser(val userId: Int, val userName: String, val userEmail: String)

// Transform between internal field names and API field names
object ApiFieldTransformer : JsonTransformingSerializer<InternalUser>(InternalUser.serializer()) {
    
    private val fieldMapping = mapOf(
        "userId" to "id",
        "userName" to "name", 
        "userEmail" to "email"
    )
    
    private val reverseMapping = fieldMapping.entries.associate { (k, v) -> v to k }
    
    override fun transformSerialize(element: JsonElement): JsonElement {
        val jsonObject = element.jsonObject
        return buildJsonObject {
            jsonObject.forEach { (key, value) ->
                val apiKey = fieldMapping[key] ?: key
                put(apiKey, value)
            }
        }
    }
    
    override fun transformDeserialize(element: JsonElement): JsonElement {
        val jsonObject = element.jsonObject
        return buildJsonObject {
            jsonObject.forEach { (key, value) ->
                val internalKey = reverseMapping[key] ?: key
                put(internalKey, value)
            }
        }
    }
}

Nested Structure Transformer:

import kotlinx.serialization.*
import kotlinx.serialization.json.*

@Serializable
data class FlatData(val name: String, val street: String, val city: String, val zip: String)

// Transform between flat structure and nested structure
object NestedTransformer : JsonTransformingSerializer<FlatData>(FlatData.serializer()) {
    
    override fun transformSerialize(element: JsonElement): JsonElement {
        val obj = element.jsonObject
        return buildJsonObject {
            put("name", obj["name"]!!)
            putJsonObject("address") {
                put("street", obj["street"]!!)
                put("city", obj["city"]!!)  
                put("zip", obj["zip"]!!)
            }
        }
    }
    
    override fun transformDeserialize(element: JsonElement): JsonElement {
        val obj = element.jsonObject
        val address = obj["address"]?.jsonObject
        
        return buildJsonObject {
            put("name", obj["name"]!!)
            if (address != null) {
                put("street", address["street"]!!)
                put("city", address["city"]!!)
                put("zip", address["zip"]!!)
            }
        }
    }
}

// Usage transforms:
// {"name":"John","street":"123 Main St","city":"Anytown","zip":"12345"}
// <-> 
// {"name":"John","address":{"street":"123 Main St","city":"Anytown","zip":"12345"}}

Custom Serializer Registration

Register custom serializers with the Json configuration.

import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlinx.serialization.modules.*

// Custom URL serializer
object UrlSerializer : KSerializer<java.net.URL> {
    override val descriptor = PrimitiveSerialDescriptor("URL", PrimitiveKind.STRING)
    override fun serialize(encoder: Encoder, value: java.net.URL) = encoder.encodeString(value.toString())
    override fun deserialize(decoder: Decoder): java.net.URL = java.net.URL(decoder.decodeString())
}

// Custom UUID serializer with JsonEncoder optimization
object UuidSerializer : KSerializer<java.util.UUID> {
    override val descriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING)
    
    override fun serialize(encoder: Encoder, value: java.util.UUID) {
        if (encoder is JsonEncoder) {
            // Optimize for JSON by storing as compact object
            encoder.encodeJsonElement(buildJsonObject {
                put("uuid", value.toString())
                put("type", "uuid")
            })
        } else {
            encoder.encodeString(value.toString())
        }
    }
    
    override fun deserialize(decoder: Decoder): java.util.UUID {
        return if (decoder is JsonDecoder) {
            val element = decoder.decodeJsonElement()
            when {
                element is JsonPrimitive -> java.util.UUID.fromString(element.content)
                element is JsonObject && "uuid" in element -> 
                    java.util.UUID.fromString(element["uuid"]!!.jsonPrimitive.content)
                else -> throw SerializationException("Invalid UUID format")
            }
        } else {
            java.util.UUID.fromString(decoder.decodeString())
        }
    }
}

// Configure Json with custom serializers
val customJson = Json {
    serializersModule = SerializersModule {
        contextual(java.net.URL::class, UrlSerializer) 
        contextual(java.util.UUID::class, UuidSerializer)
        
        // Polymorphic serializers
        polymorphic(ApiMessage::class) {
            subclass(TextMessage::class)
            subclass(ImageMessage::class)
            default { ApiMessageSerializer }
        }
    }
}

@Serializable
data class ResourceInfo(
    @Contextual val id: java.util.UUID,
    @Contextual val location: java.net.URL,
    val name: String
)

val resource = ResourceInfo(
    java.util.UUID.randomUUID(),
    java.net.URL("https://example.com/resource"),
    "Example Resource"
)

val encoded = customJson.encodeToString(resource)
val decoded = customJson.decodeFromString<ResourceInfo>(encoded)

Error Handling in Advanced Serialization

Advanced serializers should handle errors gracefully and provide meaningful error messages.

import kotlinx.serialization.*
import kotlinx.serialization.json.*

object SafeTransformer : JsonTransformingSerializer<MyData>(MyData.serializer()) {
    override fun transformDeserialize(element: JsonElement): JsonElement {
        return try {
            // Attempt transformation
            performTransformation(element)
        } catch (e: Exception) {
            // Provide context in error message
            throw SerializationException(
                "Failed to transform JSON element: ${e.message}. " +
                "Element was: $element", 
                e
            )
        }
    }
    
    private fun performTransformation(element: JsonElement): JsonElement {
        // Safe transformation logic with validation
        require(element is JsonObject) { "Expected JsonObject, got ${element::class.simpleName}" }
        
        val requiredFields = listOf("id", "name")
        val missingFields = requiredFields.filter { it !in element }
        if (missingFields.isNotEmpty()) {
            throw IllegalArgumentException("Missing required fields: $missingFields")
        }
        
        return buildJsonObject {
            element.forEach { (key, value) ->
                put(key, value)
            }
        }
    }
}

Performance Considerations

  • JsonEncoder/JsonDecoder: Provide direct access to JSON structure, avoiding intermediate object creation
  • JsonContentPolymorphicSerializer: Content inspection has overhead; use sparingly for large datasets
  • JsonTransformingSerializer: Each transformation creates new JsonElement instances; consider caching for repeated operations
  • Custom Serializers: Implement efficiently to avoid performance bottlenecks in serialization-heavy applications

Install with Tessl CLI

npx tessl i tessl/maven-org-jetbrains-kotlinx--kotlinx-serialization-json-jvm

docs

advanced.md

annotations.md

builders.md

configuration.md

index.md

json-element.md

platform.md

serialization.md

tile.json