CtrlK
BlogDocsLog inGet started
Tessl Logo

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

Kotlin multiplatform JSON serialization library with JavaScript-specific dynamic object conversion capabilities

Pending
Overview
Eval results
Files

custom-serializers.mddocs/

Custom Serializers

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.

Capabilities

JsonTransformingSerializer

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)

Property Manipulation Serializer

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)

JsonContentPolymorphicSerializer

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)

JsonEncoder and JsonDecoder Interfaces

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!")

Contextual Serialization

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"

Error Handling in Custom Serializers

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

docs

builder-dsl.md

configuration.md

core-operations.md

custom-serializers.md

dynamic-conversion.md

index.md

json-annotations.md

json-element.md

tile.json