CtrlK
BlogDocsLog inGet started
Tessl Logo

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

A Kotlin multiplatform library for JSON serialization providing type-safe JSON parsing and object serialization

Pending
Overview
Eval results
Files

annotations.mddocs/

Annotations and Naming

Annotations for customizing JSON serialization behavior and naming strategies for automatic property name transformation during serialization and deserialization.

Capabilities

@JsonNames Annotation

Specify alternative property names for JSON deserialization, allowing flexible field name matching.

/**
 * Specifies alternative names for JSON property deserialization
 * Allows a single Kotlin property to match multiple JSON field names
 */
@Target(AnnotationTarget.PROPERTY)
@SerialInfo
@ExperimentalSerializationApi
annotation class JsonNames(vararg val names: String)

Usage Examples:

import kotlinx.serialization.*
import kotlinx.serialization.json.*

@Serializable
data class User(
    @JsonNames("user_id", "userId", "ID")
    val id: Int,
    
    @JsonNames("user_name", "username", "displayName")
    val name: String,
    
    @JsonNames("email_address", "emailAddr", "mail")
    val email: String
)

val json = Json {
    useAlternativeNames = true // Must be enabled
}

// All of these JSON formats will deserialize to the same User object
val user1 = json.decodeFromString<User>("""{"user_id": 123, "user_name": "Alice", "email_address": "alice@example.com"}""")
val user2 = json.decodeFromString<User>("""{"userId": 123, "username": "Alice", "mail": "alice@example.com"}""")  
val user3 = json.decodeFromString<User>("""{"ID": 123, "displayName": "Alice", "emailAddr": "alice@example.com"}""")
val user4 = json.decodeFromString<User>("""{"id": 123, "name": "Alice", "email": "alice@example.com"}""")

// All produce the same result: User(id=123, name="Alice", email="alice@example.com")

// Serialization always uses the primary property name
val serialized = json.encodeToString(user1)
// Result: {"id":123,"name":"Alice","email":"alice@example.com"}

API Migration Example:

import kotlinx.serialization.*
import kotlinx.serialization.json.*

// Handling API evolution with backward compatibility
@Serializable
data class ProductV2(
    val id: Int,
    
    @JsonNames("product_name", "title") // Legacy field names  
    val name: String,
    
    @JsonNames("product_price", "cost", "amount")
    val price: Double,
    
    @JsonNames("product_category", "cat", "type")
    val category: String,
    
    @JsonNames("is_available", "available", "in_stock")
    val isAvailable: Boolean = true
)

val json = Json { useAlternativeNames = true }

// Can handle old API format
val oldFormat = json.decodeFromString<ProductV2>("""
{
    "id": 1,
    "product_name": "Laptop",
    "product_price": 999.99,
    "product_category": "Electronics",
    "is_available": true
}
""")

// Can handle new API format
val newFormat = json.decodeFromString<ProductV2>("""
{
    "id": 1,
    "title": "Laptop", 
    "cost": 999.99,
    "type": "Electronics",
    "in_stock": true
}
""")

@JsonClassDiscriminator Annotation

Specify a custom discriminator property name for polymorphic serialization at the class level.

/**
 * Specifies custom class discriminator property name for polymorphic serialization
 * Applied to sealed classes or interfaces to override the default discriminator
 */
@Target(AnnotationTarget.CLASS)
@InheritableSerialInfo
@ExperimentalSerializationApi
annotation class JsonClassDiscriminator(val discriminator: String)

Usage Examples:

import kotlinx.serialization.*
import kotlinx.serialization.json.*

@Serializable
@JsonClassDiscriminator("messageType")
sealed class ChatMessage {
    abstract val timestamp: Long
}

@Serializable
@SerialName("text")
data class TextMessage(
    override val timestamp: Long,
    val content: String,
    val author: String
) : ChatMessage()

@Serializable
@SerialName("image")
data class ImageMessage(
    override val timestamp: Long,
    val imageUrl: String,
    val caption: String?,
    val author: String
) : ChatMessage()

@Serializable
@SerialName("system")
data class SystemMessage(
    override val timestamp: Long,
    val event: String,
    val details: Map<String, String> = emptyMap()
) : ChatMessage()

val messages = listOf<ChatMessage>(
    TextMessage(System.currentTimeMillis(), "Hello everyone!", "Alice"),
    ImageMessage(System.currentTimeMillis(), "https://example.com/photo.jpg", "Beautiful sunset", "Bob"),
    SystemMessage(System.currentTimeMillis(), "USER_JOINED", mapOf("username" to "Charlie"))
)

val json = Json.encodeToString(messages)
// Result uses "messageType" instead of default "type":
// [
//   {"messageType":"text","timestamp":1234567890,"content":"Hello everyone!","author":"Alice"},
//   {"messageType":"image","timestamp":1234567891,"imageUrl":"https://example.com/photo.jpg","caption":"Beautiful sunset","author":"Bob"},
//   {"messageType":"system","timestamp":1234567892,"event":"USER_JOINED","details":{"username":"Charlie"}}
// ]

val decoded = Json.decodeFromString<List<ChatMessage>>(json)

Nested Class Discriminators:

import kotlinx.serialization.*
import kotlinx.serialization.json.*

@Serializable
@JsonClassDiscriminator("shapeType")
sealed class Shape {
    abstract val area: Double
}

@Serializable
@SerialName("circle")
data class Circle(val radius: Double) : Shape() {
    override val area: Double get() = 3.14159 * radius * radius
}

@Serializable  
@JsonClassDiscriminator("vehicleKind")
sealed class Vehicle {
    abstract val maxSpeed: Int
}

@Serializable
@SerialName("car")
data class Car(override val maxSpeed: Int, val doors: Int) : Vehicle()

@Serializable
data class DrawingObject(
    val id: String,
    val shape: Shape,
    val vehicle: Vehicle? = null
)

// Each polymorphic hierarchy uses its own discriminator
val obj = DrawingObject(
    "obj1",
    Circle(5.0),
    Car(120, 4)
)

val json = Json.encodeToString(obj)
// Result: 
// {
//   "id": "obj1",
//   "shape": {"shapeType": "circle", "radius": 5.0},
//   "vehicle": {"vehicleKind": "car", "maxSpeed": 120, "doors": 4}
// }

@JsonIgnoreUnknownKeys Annotation

Ignore unknown JSON properties for a specific class during deserialization, overriding the global Json configuration.

/**
 * Ignore unknown properties during deserialization for annotated class
 * Overrides the global ignoreUnknownKeys setting for this specific class
 */
@Target(AnnotationTarget.CLASS)
@SerialInfo
@ExperimentalSerializationApi
annotation class JsonIgnoreUnknownKeys

Usage Examples:

import kotlinx.serialization.*
import kotlinx.serialization.json.*

// Strict class - will fail on unknown properties
@Serializable
data class StrictConfig(
    val timeout: Int,
    val enabled: Boolean
)

// Flexible class - ignores unknown properties
@Serializable
@JsonIgnoreUnknownKeys
data class FlexibleConfig(
    val timeout: Int,
    val enabled: Boolean
)

@Serializable
data class AppSettings(
    val strict: StrictConfig,
    val flexible: FlexibleConfig
)

// Global Json configuration is strict
val json = Json {
    ignoreUnknownKeys = false // Strict by default
}

val jsonString = """
{
    "strict": {
        "timeout": 30,
        "enabled": true,
        "extra": "this will cause error"
    },
    "flexible": {
        "timeout": 60, 
        "enabled": false,
        "extra": "this will be ignored",
        "moreExtra": "this too"
    }
}
"""

try {
    val settings = json.decodeFromString<AppSettings>(jsonString)
    // This will fail because StrictConfig doesn't ignore unknown keys
} catch (e: SerializationException) {
    println("Error: ${e.message}") // Unknown key 'extra'
}

// Fix the JSON for strict config
val fixedJsonString = """
{
    "strict": {
        "timeout": 30,
        "enabled": true
    },
    "flexible": {
        "timeout": 60,
        "enabled": false, 
        "extra": "this will be ignored",
        "moreExtra": "this too"
    }
}
"""

val settings = json.decodeFromString<AppSettings>(fixedJsonString)
// Success: FlexibleConfig ignores extra fields, StrictConfig has no extra fields

JsonNamingStrategy Interface

Strategy interface for automatic property name transformation during serialization.

/**
 * Strategy for transforming property names during JSON serialization
 * Provides automatic name transformation without manual field annotations
 */
@ExperimentalSerializationApi
fun interface JsonNamingStrategy {
    /**
     * Transform a property name for JSON serialization
     * @param descriptor Serial descriptor of the containing class
     * @param elementIndex Index of the property in the descriptor
     * @param serialName Original property name from Kotlin
     * @return Transformed property name for JSON
     */
    fun serialNameForJson(
        descriptor: SerialDescriptor,
        elementIndex: Int, 
        serialName: String
    ): String
    
    companion object {
        /**
         * Converts camelCase property names to snake_case
         */
        val SnakeCase: JsonNamingStrategy
        
        /**
         * Converts camelCase property names to kebab-case
         */
        val KebabCase: JsonNamingStrategy
    }
}

Built-in Strategies:

import kotlinx.serialization.*
import kotlinx.serialization.json.*

@Serializable
data class UserPreferences(
    val darkModeEnabled: Boolean,
    val autoSaveInterval: Int,
    val notificationSettings: NotificationSettings,
    val preferredLanguage: String
)

@Serializable
data class NotificationSettings(
    val emailNotifications: Boolean,
    val pushNotifications: Boolean,
    val soundEnabled: Boolean
)

val preferences = UserPreferences(
    darkModeEnabled = true,
    autoSaveInterval = 300,
    notificationSettings = NotificationSettings(
        emailNotifications = true,
        pushNotifications = false,
        soundEnabled = true
    ),
    preferredLanguage = "en-US"
)

// Snake case transformation
val snakeCaseJson = Json {
    namingStrategy = JsonNamingStrategy.SnakeCase
}
val snakeCase = snakeCaseJson.encodeToString(preferences)
// Result:
// {
//   "dark_mode_enabled": true,
//   "auto_save_interval": 300,
//   "notification_settings": {
//     "email_notifications": true,
//     "push_notifications": false,
//     "sound_enabled": true
//   },
//   "preferred_language": "en-US"
// }

// Kebab case transformation  
val kebabCaseJson = Json {
    namingStrategy = JsonNamingStrategy.KebabCase
}
val kebabCase = kebabCaseJson.encodeToString(preferences)
// Result:
// {
//   "dark-mode-enabled": true,
//   "auto-save-interval": 300,
//   "notification-settings": {
//     "email-notifications": true,
//     "push-notifications": false,
//     "sound-enabled": true
//   },
//   "preferred-language": "en-US"
// }

// Deserialization works with the same naming strategy
val decodedSnake = snakeCaseJson.decodeFromString<UserPreferences>(snakeCase)
val decodedKebab = kebabCaseJson.decodeFromString<UserPreferences>(kebabCase)

Custom Naming Strategy:

import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlinx.serialization.descriptors.*

// Custom strategy: UPPERCASE property names
object UpperCaseNamingStrategy : JsonNamingStrategy {
    override fun serialNameForJson(
        descriptor: SerialDescriptor,
        elementIndex: Int,
        serialName: String
    ): String = serialName.uppercase()
}

// Custom strategy: Add prefix based on class name
class PrefixNamingStrategy(private val prefix: String) : JsonNamingStrategy {
    override fun serialNameForJson(
        descriptor: SerialDescriptor,
        elementIndex: Int,
        serialName: String
    ): String = "${prefix}_$serialName"
}

// Custom strategy: Conditional transformation
object ApiNamingStrategy : JsonNamingStrategy {
    override fun serialNameForJson(
        descriptor: SerialDescriptor,
        elementIndex: Int,
        serialName: String
    ): String {
        return when {
            serialName.endsWith("Id") -> serialName.lowercase()
            serialName.startsWith("is") -> serialName.removePrefix("is").lowercase()
            serialName.contains("Url") -> serialName.replace("Url", "URL")
            else -> serialName.lowercase()
        }
    }
}

@Serializable
data class ApiUser(
    val userId: Int,
    val isActive: Boolean,
    val profileUrl: String,
    val displayName: String
)

val user = ApiUser(123, true, "https://example.com/profile.jpg", "Alice")

val customJson = Json {
    namingStrategy = ApiNamingStrategy
}
val result = customJson.encodeToString(user)
// Result: {"userid":123,"active":true,"profileURL":"https://example.com/profile.jpg","displayname":"Alice"}

Combining Annotations and Naming Strategies

Annotations take precedence over naming strategies for specific properties.

import kotlinx.serialization.*
import kotlinx.serialization.json.*

@Serializable
data class MixedNamingExample(
    // Uses naming strategy transformation
    val firstName: String,
    
    // Override with explicit @SerialName
    @SerialName("family_name")
    val lastName: String,
    
    // Override with @JsonNames for flexibility
    @JsonNames("user_email", "email_addr") 
    val emailAddress: String,
    
    // Uses naming strategy transformation
    val phoneNumber: String?
)

val json = Json {
    namingStrategy = JsonNamingStrategy.SnakeCase
    useAlternativeNames = true
}

val person = MixedNamingExample(
    "John", 
    "Doe", 
    "john@example.com", 
    "555-1234"
)

val encoded = json.encodeToString(person)
// Result: {
//   "first_name": "John",        // Transformed by naming strategy
//   "family_name": "Doe",        // Uses explicit @SerialName
//   "email_address": "john@example.com",  // Uses property name (not alternative names in serialization)
//   "phone_number": "555-1234"   // Transformed by naming strategy
// }

// Can decode using alternative names
val alternativeJson = """
{
    "first_name": "Jane",
    "family_name": "Smith", 
    "user_email": "jane@example.com",
    "phone_number": "555-5678"
}
"""

val decoded = json.decodeFromString<MixedNamingExample>(alternativeJson)
// Success: uses "user_email" alternative name for emailAddress

Best Practices

When to Use Each Annotation

  • @JsonNames: API evolution, supporting multiple client versions, integrating with inconsistent third-party APIs
  • @JsonClassDiscriminator: Domain-specific discriminator names that are more meaningful than "type"
  • @JsonIgnoreUnknownKeys: Classes that need to be forward-compatible with API changes
  • JsonNamingStrategy: Consistent naming convention across entire API without manual annotations

Performance Considerations

  • @JsonNames: Minimal performance impact, names are resolved at compile time
  • JsonNamingStrategy: Applied to every property during serialization/deserialization
  • @JsonIgnoreUnknownKeys: No performance impact, just skips unknown properties
  • @JsonClassDiscriminator: No performance impact, uses standard polymorphic serialization

Install with Tessl CLI

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

docs

advanced.md

annotations.md

builders.md

configuration.md

index.md

json-element.md

platform.md

serialization.md

tile.json