Kotlin multiplatform JSON serialization library with type-safe, reflectionless approach supporting all platforms
—
Interfaces and base classes for implementing custom JSON serialization logic with access to JsonElement representations.
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)
}
}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)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}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