A Kotlin multiplatform library for JSON serialization providing type-safe JSON parsing and object serialization
—
Annotations for customizing JSON serialization behavior and naming strategies for automatic property name transformation during serialization and deserialization.
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
}
""")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}
// }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 JsonIgnoreUnknownKeysUsage 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 fieldsStrategy 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"}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 emailAddressInstall with Tessl CLI
npx tessl i tessl/maven-org-jetbrains-kotlinx--kotlinx-serialization-json-jvm