A Kotlin multiplatform library for JSON serialization providing type-safe JSON parsing and object serialization
—
Advanced serialization interfaces, custom serializers, and low-level JSON processing APIs for specialized use cases requiring fine-grained control over serialization behavior.
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)
}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)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)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 deserializationField 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"}}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)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)
}
}
}
}Install with Tessl CLI
npx tessl i tessl/maven-org-jetbrains-kotlinx--kotlinx-serialization-json-jvm