CtrlK
BlogDocsLog inGet started
Tessl Logo

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

Kotlin multiplatform JSON serialization library with type-safe, reflectionless approach supporting all platforms

Pending
Overview
Eval results
Files

custom-serializers.mddocs/

Custom Serializers

Interfaces and base classes for implementing custom JSON serialization logic with access to JsonElement representations.

Capabilities

JsonEncoder Interface

Encoder interface providing access to Json instance and JsonElement encoding capability.

/**
 * Encoder used by Json during serialization
 * Provides access to Json instance and direct JsonElement encoding
 */
interface JsonEncoder : Encoder, CompositeEncoder {
    /** An instance of the current Json */
    val json: Json
    
    /**
     * Appends the given JSON element to the current output
     * This method should only be used as part of the whole serialization process
     * @param element JsonElement to encode directly
     */
    fun encodeJsonElement(element: JsonElement)
}

Usage Examples:

@Serializable(with = CustomDataSerializer::class)
data class CustomData(val value: String, val metadata: Map<String, Any>)

object CustomDataSerializer : KSerializer<CustomData> {
    override val descriptor = buildClassSerialDescriptor("CustomData") {
        element<String>("value")
        element<JsonElement>("metadata")
    }
    
    override fun serialize(encoder: Encoder, value: CustomData) {
        val output = encoder as? JsonEncoder 
            ?: throw SerializationException("This serializer can only be used with Json format")
            
        val element = buildJsonObject {
            put("value", value.value)
            put("timestamp", System.currentTimeMillis())
            
            putJsonObject("metadata") {
                value.metadata.forEach { (key, metaValue) ->
                    when (metaValue) {
                        is String -> put(key, metaValue)
                        is Number -> put(key, metaValue) 
                        is Boolean -> put(key, metaValue)
                        else -> put(key, metaValue.toString())
                    }
                }
            }
        }
        
        output.encodeJsonElement(element)
    }
    
    override fun deserialize(decoder: Decoder): CustomData {
        val input = decoder as? JsonDecoder 
            ?: throw SerializationException("This serializer can only be used with Json format")
            
        val element = input.decodeJsonElement().jsonObject
        val value = element["value"]?.jsonPrimitive?.content 
            ?: throw SerializationException("Missing 'value' field")
            
        val metadata = element["metadata"]?.jsonObject?.mapValues { (_, jsonValue) ->
            when (jsonValue) {
                is JsonPrimitive -> when {
                    jsonValue.isString -> jsonValue.content
                    jsonValue.booleanOrNull != null -> jsonValue.boolean
                    jsonValue.longOrNull != null -> jsonValue.long
                    jsonValue.doubleOrNull != null -> jsonValue.double
                    else -> jsonValue.content
                }
                else -> jsonValue.toString()
            }
        } ?: emptyMap()
        
        return CustomData(value, metadata)
    }
}

JsonDecoder Interface

Decoder interface providing access to Json instance and JsonElement decoding capability.

/**
 * Decoder used by Json during deserialization
 * Provides access to Json instance and direct JsonElement decoding
 */
interface JsonDecoder : Decoder, CompositeDecoder {
    /** An instance of the current Json */
    val json: Json
    
    /**
     * Decodes the next element in the current input as JsonElement
     * This method should only be used as part of the whole deserialization process
     * @return JsonElement representation of current input
     */
    fun decodeJsonElement(): JsonElement
}

Usage Examples:

// Conditional deserialization based on JSON content
@Serializable(with = FlexibleResponseSerializer::class)
sealed class ApiResponse {
    @Serializable
    data class Success(val data: JsonElement) : ApiResponse()
    
    @Serializable  
    data class Error(val message: String, val code: Int) : ApiResponse()
}

object FlexibleResponseSerializer : KSerializer<ApiResponse> {
    override val descriptor = buildSerialDescriptor("ApiResponse", PolymorphicKind.SEALED)
    
    override fun serialize(encoder: Encoder, value: ApiResponse) {
        val output = encoder as JsonEncoder
        val element = when (value) {
            is ApiResponse.Success -> buildJsonObject {
                put("success", true)
                put("data", value.data)
            }
            is ApiResponse.Error -> buildJsonObject {
                put("success", false)
                put("error", buildJsonObject {
                    put("message", value.message)
                    put("code", value.code)
                })
            }
        }
        output.encodeJsonElement(element)
    }
    
    override fun deserialize(decoder: Decoder): ApiResponse {
        val input = decoder as JsonDecoder
        val element = input.decodeJsonElement().jsonObject
        
        val success = element["success"]?.jsonPrimitive?.boolean ?: false
        
        return if (success) {
            val data = element["data"] ?: JsonNull
            ApiResponse.Success(data)
        } else {
            val errorObj = element["error"]?.jsonObject 
                ?: throw SerializationException("Missing error object")
            val message = errorObj["message"]?.jsonPrimitive?.content 
                ?: throw SerializationException("Missing error message")
            val code = errorObj["code"]?.jsonPrimitive?.int 
                ?: throw SerializationException("Missing error code")
            ApiResponse.Error(message, code)
        }
    }
}

// Usage
val json = Json { ignoreUnknownKeys = true }

val successJson = """{"success":true,"data":{"user":"Alice","score":100}}"""
val errorJson = """{"success":false,"error":{"message":"Not found","code":404}}"""

val successResponse = json.decodeFromString<ApiResponse>(successJson)
val errorResponse = json.decodeFromString<ApiResponse>(errorJson)

JsonTransformingSerializer

Abstract base class for serializers that transform JsonElement during serialization/deserialization.

/**
 * Base class for custom serializers that manipulate JsonElement representation
 * before serialization or after deserialization
 * @param T Type for Kotlin property this serializer applies to
 * @param tSerializer Serializer for type T
 */
abstract class JsonTransformingSerializer<T : Any?>(
    private val tSerializer: KSerializer<T>
) : KSerializer<T> {
    
    /** Descriptor delegates to tSerializer's descriptor by default */
    override val descriptor: SerialDescriptor get() = tSerializer.descriptor
    
    /**
     * Transformation applied during deserialization
     * JsonElement from input is transformed before being passed to tSerializer
     * @param element Original JsonElement from input
     * @return Transformed JsonElement for deserialization
     */
    protected open fun transformDeserialize(element: JsonElement): JsonElement = element
    
    /**
     * Transformation applied during serialization  
     * JsonElement from tSerializer is transformed before output
     * @param element JsonElement produced by tSerializer
     * @return Transformed JsonElement for output
     */
    protected open fun transformSerialize(element: JsonElement): JsonElement = element
}

Usage Examples:

// Transform list to single object and vice versa
@Serializable
data class UserPreferences(@Serializable(UnwrappingListSerializer::class) val theme: String)

object UnwrappingListSerializer : JsonTransformingSerializer<String>(String.serializer()) {
    override fun transformDeserialize(element: JsonElement): JsonElement {
        // If input is array with single element, unwrap it
        return if (element is JsonArray && element.size == 1) {
            element.first()
        } else {
            element
        }
    }
    
    override fun transformSerialize(element: JsonElement): JsonElement {
        // Wrap single string in array for output
        return buildJsonArray { add(element) }
    }
}

// Usage: Both inputs deserialize to same result
val prefs1 = json.decodeFromString<UserPreferences>("""{"theme":"dark"}""")
val prefs2 = json.decodeFromString<UserPreferences>("""{"theme":["dark"]}""")
// Both create UserPreferences(theme="dark")

// But serialization always produces array format
val output = json.encodeToString(prefs1)
// {"theme":["dark"]}

// Normalize number formats
object NormalizedNumberSerializer : JsonTransformingSerializer<Double>(Double.serializer()) {
    override fun transformDeserialize(element: JsonElement): JsonElement {
        // Accept both string and number representations
        return when (element) {
            is JsonPrimitive -> {
                if (element.isString) {
                    val number = element.content.toDoubleOrNull()
                    if (number != null) JsonPrimitive(number) else element
                } else element
            }
            else -> element
        }
    }
    
    override fun transformSerialize(element: JsonElement): JsonElement {
        // Always output as number, never as string
        return if (element is JsonPrimitive && element.isString) {
            val number = element.content.toDoubleOrNull()
            if (number != null) JsonPrimitive(number) else element
        } else element
    }
}

@Serializable
data class Measurement(@Serializable(NormalizedNumberSerializer::class) val value: Double)

// Accepts both formats
val measurement1 = json.decodeFromString<Measurement>("""{"value":123.45}""")
val measurement2 = json.decodeFromString<Measurement>("""{"value":"123.45"}""")
// Both create Measurement(value=123.45)

// Always outputs as number
val output = json.encodeToString(measurement1)
// {"value":123.45}

JsonContentPolymorphicSerializer

Abstract base class for polymorphic serializers that select deserializer based on JSON content.

/**
 * Base class for custom serializers that select polymorphic serializer
 * based on JSON content rather than class discriminator
 * @param T Root type for polymorphic class hierarchy
 * @param baseClass Class token for T
 */
abstract class JsonContentPolymorphicSerializer<T : Any>(
    private val baseClass: KClass<T>
) : KSerializer<T> {
    
    /** Descriptor with polymorphic kind */
    override val descriptor: SerialDescriptor
    
    /**
     * Determines deserialization strategy by examining parsed JSON element
     * @param element JsonElement to examine for determining type
     * @return DeserializationStrategy for the appropriate subtype
     */
    protected abstract fun selectDeserializer(element: JsonElement): DeserializationStrategy<T>
}

Usage Examples:

@Serializable
sealed class PaymentMethod {
    @Serializable
    data class CreditCard(val number: String, val expiry: String) : PaymentMethod()
    
    @Serializable
    data class BankTransfer(val accountNumber: String, val routingNumber: String) : PaymentMethod()
    
    @Serializable
    data class DigitalWallet(val walletId: String, val provider: String) : PaymentMethod()
}

object PaymentMethodSerializer : JsonContentPolymorphicSerializer<PaymentMethod>(PaymentMethod::class) {
    override fun selectDeserializer(element: JsonElement): DeserializationStrategy<PaymentMethod> {
        val obj = element.jsonObject
        return when {
            "number" in obj && "expiry" in obj -> PaymentMethod.CreditCard.serializer()
            "accountNumber" in obj && "routingNumber" in obj -> PaymentMethod.BankTransfer.serializer()
            "walletId" in obj && "provider" in obj -> PaymentMethod.DigitalWallet.serializer()
            else -> throw SerializationException("Unknown payment method type")
        }
    }
}

// Register the serializer
val json = Json {
    serializersModule = SerializersModule {
        polymorphic(PaymentMethod::class) {
            default { PaymentMethodSerializer }
        }
    }
}

// Usage - no type discriminator needed in JSON
val creditCardJson = """{"number":"1234-5678-9012-3456","expiry":"12/25"}"""
val bankTransferJson = """{"accountNumber":"123456789","routingNumber":"987654321"}"""
val walletJson = """{"walletId":"user123","provider":"PayPal"}"""

val creditCard = json.decodeFromString<PaymentMethod>(creditCardJson)
val bankTransfer = json.decodeFromString<PaymentMethod>(bankTransferJson)  
val wallet = json.decodeFromString<PaymentMethod>(walletJson)

// Complex content-based selection
@Serializable
sealed class DatabaseConfig {
    @Serializable
    data class MySQL(val host: String, val port: Int = 3306, val charset: String = "utf8") : DatabaseConfig()
    
    @Serializable
    data class PostgreSQL(val host: String, val port: Int = 5432, val schema: String = "public") : DatabaseConfig()
    
    @Serializable
    data class MongoDB(val connectionString: String, val database: String) : DatabaseConfig()
}

object DatabaseConfigSerializer : JsonContentPolymorphicSerializer<DatabaseConfig>(DatabaseConfig::class) {
    override fun selectDeserializer(element: JsonElement): DeserializationStrategy<DatabaseConfig> {
        val obj = element.jsonObject
        return when {
            "connectionString" in obj -> DatabaseConfig.MongoDB.serializer()
            "charset" in obj -> DatabaseConfig.MySQL.serializer()
            "schema" in obj -> DatabaseConfig.PostgreSQL.serializer()
            obj["port"]?.jsonPrimitive?.int == 3306 -> DatabaseConfig.MySQL.serializer()
            obj["port"]?.jsonPrimitive?.int == 5432 -> DatabaseConfig.PostgreSQL.serializer()
            else -> DatabaseConfig.PostgreSQL.serializer() // Default
        }
    }
}

Install with Tessl CLI

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

docs

annotations.md

configuration.md

core-operations.md

custom-serializers.md

dsl-builders.md

index.md

json-elements.md

naming-strategies.md

platform-extensions.md

tile.json