Kotlin multiplatform JSON serialization library with JavaScript-specific dynamic object conversion capabilities
—
Base classes and interfaces for implementing custom JSON serialization logic with transformation and polymorphic capabilities. These provide powerful extension points for handling complex serialization scenarios.
Abstract base class for creating serializers that transform JsonElement structures during serialization/deserialization.
/**
* Base class for JSON transformation serializers
* @param tSerializer The underlying serializer for type T
*/
abstract class JsonTransformingSerializer<T>(private val tSerializer: KSerializer<T>) : KSerializer<T> {
/**
* Transform JsonElement during serialization (encode)
* @param element JsonElement to transform
* @return Transformed JsonElement
*/
protected open fun transformSerialize(element: JsonElement): JsonElement = element
/**
* Transform JsonElement during deserialization (decode)
* @param element JsonElement to transform
* @return Transformed JsonElement
*/
protected open fun transformDeserialize(element: JsonElement): JsonElement = element
}Usage Examples:
@Serializable
data class Coordinates(val x: Double, val y: Double)
// Transform coordinates between different coordinate systems
object CoordinateTransformer : JsonTransformingSerializer<Coordinates>(Coordinates.serializer()) {
override fun transformSerialize(element: JsonElement): JsonElement {
val obj = element.jsonObject
val x = obj["x"]?.jsonPrimitive?.double ?: 0.0
val y = obj["y"]?.jsonPrimitive?.double ?: 0.0
// Convert from internal coordinate system to API coordinate system
// Example: scale by 100 and offset
return buildJsonObject {
put("x", x * 100 + 1000)
put("y", y * 100 + 2000)
}
}
override fun transformDeserialize(element: JsonElement): JsonElement {
val obj = element.jsonObject
val x = obj["x"]?.jsonPrimitive?.double ?: 0.0
val y = obj["y"]?.jsonPrimitive?.double ?: 0.0
// Convert from API coordinate system to internal coordinate system
return buildJsonObject {
put("x", (x - 1000) / 100)
put("y", (y - 2000) / 100)
}
}
}
// Usage
val json = Json.Default
val coords = Coordinates(5.0, 10.0)
// Serialize with transformation
val serialized = json.encodeToString(CoordinateTransformer, coords)
// Result: {"x":1500.0,"y":3000.0} (transformed values)
// Deserialize with transformation
val apiCoords = """{"x":1500.0,"y":3000.0}"""
val deserialized = json.decodeFromString(CoordinateTransformer, apiCoords)
// Result: Coordinates(x=5.0, y=10.0) (original values restored)Transform property names or values during serialization.
Usage Examples:
@Serializable
data class User(val firstName: String, val lastName: String, val age: Int)
// Serializer that wraps user data in an envelope
object UserEnvelopeSerializer : JsonTransformingSerializer<User>(User.serializer()) {
override fun transformSerialize(element: JsonElement): JsonElement {
return buildJsonObject {
put("user_data", element)
put("metadata", buildJsonObject {
put("serialized_at", System.currentTimeMillis())
put("version", "1.0")
})
}
}
override fun transformDeserialize(element: JsonElement): JsonElement {
val obj = element.jsonObject
// Extract user data from envelope
return obj["user_data"] ?: obj // Fallback to original if no envelope
}
}
// Usage
val user = User("Alice", "Smith", 30)
val enveloped = json.encodeToString(UserEnvelopeSerializer, user)
// Result: {
// "user_data": {"firstName":"Alice","lastName":"Smith","age":30},
// "metadata": {"serialized_at":1234567890,"version":"1.0"}
// }
val restored = json.decodeFromString(UserEnvelopeSerializer, enveloped)
// Result: User(firstName="Alice", lastName="Smith", age=30)Abstract class for polymorphic serialization based on JSON content inspection.
/**
* Polymorphic serializer that selects implementation based on JSON content
* @param baseClass Base class for polymorphism
*/
abstract class JsonContentPolymorphicSerializer<T : Any>(private val baseClass: KClass<T>) : AbstractPolymorphicSerializer<T>() {
/**
* Select deserializer based on JsonElement content
* @param element JsonElement to inspect
* @return Deserialization strategy for the specific type
*/
protected abstract fun selectDeserializer(element: JsonElement): DeserializationStrategy<T>
}Usage Examples:
@Serializable
abstract class Shape {
abstract val area: Double
}
@Serializable
data class Circle(val radius: Double) : Shape() {
override val area: Double get() = 3.14159 * radius * radius
}
@Serializable
data class Rectangle(val width: Double, val height: Double) : Shape() {
override val area: Double get() = width * height
}
@Serializable
data class Triangle(val base: Double, val height: Double) : Shape() {
override val area: Double get() = 0.5 * base * height
}
// Content-based polymorphic serializer
object ShapeSerializer : JsonContentPolymorphicSerializer<Shape>(Shape::class) {
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<Shape> {
val obj = element.jsonObject
return when {
"radius" in obj -> Circle.serializer()
"width" in obj && "height" in obj -> Rectangle.serializer()
"base" in obj && "height" in obj -> Triangle.serializer()
else -> throw JsonDecodingException("Unknown shape type: ${obj.keys}")
}
}
}
// Usage
val shapes = listOf(
Circle(5.0),
Rectangle(4.0, 6.0),
Triangle(3.0, 8.0)
)
val json = Json.Default
// Serialize different shapes
shapes.forEach { shape ->
val serialized = json.encodeToString(ShapeSerializer, shape)
println("Serialized: $serialized")
val deserialized = json.decodeFromString(ShapeSerializer, serialized)
println("Deserialized: $deserialized")
println("Area: ${deserialized.area}")
println()
}
// Handle JSON without discriminator
val circleJson = """{"radius":10.0}"""
val circle = json.decodeFromString(ShapeSerializer, circleJson)
// Result: Circle(radius=10.0)
val rectangleJson = """{"width":5.0,"height":3.0}"""
val rectangle = json.decodeFromString(ShapeSerializer, rectangleJson)
// Result: Rectangle(width=5.0, height=3.0)Access JSON-specific encoding and decoding capabilities in custom serializers.
/**
* JSON-specific encoder interface
*/
interface JsonEncoder : Encoder, CompositeEncoder {
/**
* Json instance being used for encoding
*/
val json: Json
/**
* Encode JsonElement directly
* @param element JsonElement to encode
*/
fun encodeJsonElement(element: JsonElement)
}
/**
* JSON-specific decoder interface
*/
interface JsonDecoder : Decoder, CompositeDecoder {
/**
* Json instance being used for decoding
*/
val json: Json
/**
* Decode current value as JsonElement
* @return JsonElement representation
*/
fun decodeJsonElement(): JsonElement
}Usage Examples:
@Serializable
data class FlexibleData(val content: String)
// Custom serializer using JsonEncoder/JsonDecoder
object FlexibleDataSerializer : KSerializer<FlexibleData> {
override val descriptor = buildClassSerialDescriptor("FlexibleData") {
element<String>("content")
}
override fun serialize(encoder: Encoder, value: FlexibleData) {
if (encoder is JsonEncoder) {
// Use JSON-specific functionality
val element = buildJsonObject {
put("content", value.content)
put("serialized_with", "custom_serializer")
put("timestamp", System.currentTimeMillis())
}
encoder.encodeJsonElement(element)
} else {
// Fallback for non-JSON formats
encoder.encodeString(value.content)
}
}
override fun deserialize(decoder: Decoder): FlexibleData {
return if (decoder is JsonDecoder) {
// Use JSON-specific functionality
val element = decoder.decodeJsonElement()
val obj = element.jsonObject
val content = obj["content"]?.jsonPrimitive?.content
?: throw JsonDecodingException("Missing content field")
FlexibleData(content)
} else {
// Fallback for non-JSON formats
FlexibleData(decoder.decodeString())
}
}
}
// Usage
val data = FlexibleData("Hello, World!")
val json = Json { prettyPrint = true }
val serialized = json.encodeToString(FlexibleDataSerializer, data)
// Result: {
// "content": "Hello, World!",
// "serialized_with": "custom_serializer",
// "timestamp": 1234567890
// }
val deserialized = json.decodeFromString(FlexibleDataSerializer, serialized)
// Result: FlexibleData(content="Hello, World!")Access serialization context and configuration in custom serializers.
Usage Examples:
// Serializer that adapts behavior based on Json configuration
object AdaptiveStringSerializer : KSerializer<String> {
override val descriptor = PrimitiveSerialDescriptor("AdaptiveString", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: String) {
if (encoder is JsonEncoder) {
val json = encoder.json
val element = if (json.configuration.prettyPrint) {
// If pretty printing is enabled, add formatting hints
JsonPrimitive("FORMATTED: $value")
} else {
JsonPrimitive(value)
}
encoder.encodeJsonElement(element)
} else {
encoder.encodeString(value)
}
}
override fun deserialize(decoder: Decoder): String {
return if (decoder is JsonDecoder) {
val element = decoder.decodeJsonElement()
val content = element.jsonPrimitive.content
// Remove formatting prefix if present
content.removePrefix("FORMATTED: ")
} else {
decoder.decodeString()
}
}
}
// Usage with different Json configurations
val compactJson = Json.Default
val prettyJson = Json { prettyPrint = true }
val text = "Hello"
val compactResult = compactJson.encodeToString(AdaptiveStringSerializer, text)
// Result: "Hello"
val prettyResult = prettyJson.encodeToString(AdaptiveStringSerializer, text)
// Result: "FORMATTED: Hello"
// Both deserialize to the same value
val restored1 = compactJson.decodeFromString(AdaptiveStringSerializer, compactResult)
val restored2 = prettyJson.decodeFromString(AdaptiveStringSerializer, prettyResult)
// Both result in: "Hello"Proper error handling patterns for custom serializers.
Usage Examples:
object SafeIntSerializer : KSerializer<Int> {
override val descriptor = PrimitiveSerialDescriptor("SafeInt", PrimitiveKind.INT)
override fun serialize(encoder: Encoder, value: Int) {
encoder.encodeInt(value)
}
override fun deserialize(decoder: Decoder): Int {
return try {
if (decoder is JsonDecoder) {
val element = decoder.decodeJsonElement()
when (val primitive = element.jsonPrimitive) {
is JsonNull -> 0 // Default value for null
else -> {
// Try to parse as int, with fallback handling
primitive.intOrNull
?: primitive.doubleOrNull?.toInt()
?: primitive.content.toIntOrNull()
?: throw JsonDecodingException("Cannot convert '${primitive.content}' to Int")
}
}
} else {
decoder.decodeInt()
}
} catch (e: NumberFormatException) {
throw JsonDecodingException("Invalid number format: ${e.message}")
} catch (e: IllegalArgumentException) {
throw JsonDecodingException("Invalid integer value: ${e.message}")
}
}
}
// Usage - handles various input formats gracefully
val json = Json.Default
val validInputs = listOf(
"42", // String number
"42.0", // Float that converts to int
"null" // Null converts to 0
)
validInputs.forEach { input ->
try {
val result = json.decodeFromString(SafeIntSerializer, input)
println("'$input' -> $result")
} catch (e: JsonDecodingException) {
println("'$input' -> Error: ${e.message}")
}
}Install with Tessl CLI
npx tessl i tessl/maven-org-jetbrains-kotlinx--kotlinx-serialization-json-js