CtrlK
BlogDocsLog inGet started
Tessl Logo

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

Kotlin multiplatform JSON serialization library with type-safe, reflectionless approach supporting all platforms

Pending
Overview
Eval results
Files

naming-strategies.mddocs/

Naming Strategies

Built-in naming strategies for transforming property names during serialization/deserialization.

Capabilities

JsonNamingStrategy Interface

Function interface for defining custom property name transformations.

/**
 * Represents naming strategy - a transformer for serial names in Json format
 * Transformed serial names are used for both serialization and deserialization
 * Applied globally in Json configuration builder
 */
fun interface JsonNamingStrategy {
    /**
     * Accepts original serialName and returns transformed name for JSON
     * @param descriptor SerialDescriptor of the containing class
     * @param elementIndex Index of the element in the descriptor
     * @param serialName Original serial name (property name or @SerialName value)
     * @return Transformed serial name for JSON encoding/decoding
     */
    fun serialNameForJson(descriptor: SerialDescriptor, elementIndex: Int, serialName: String): String
}

Built-in Naming Strategies

Pre-built naming strategies for common naming conventions.

/**
 * Built-in naming strategies companion object
 */
companion object Builtins {
    /**
     * Strategy that transforms serial names from camelCase to snake_case
     * Words' bounds are defined by uppercase characters
     */
    val SnakeCase: JsonNamingStrategy
    
    /**
     * Strategy that transforms serial names from camelCase to kebab-case
     * Words' bounds are defined by uppercase characters  
     */
    val KebabCase: JsonNamingStrategy
}

SnakeCase Strategy

Transforms camelCase property names to snake_case format.

Usage Examples:

@Serializable
data class UserAccount(
    val userId: Int,
    val firstName: String,
    val lastName: String,
    val emailAddress: String,
    val isActive: Boolean,
    val createdAt: Long,
    val lastLoginTime: Long?
)

// Configure Json with snake_case naming
val snakeCaseJson = Json {
    namingStrategy = JsonNamingStrategy.SnakeCase
}

val account = UserAccount(
    userId = 123,
    firstName = "Alice",
    lastName = "Smith", 
    emailAddress = "alice.smith@example.com",
    isActive = true,
    createdAt = 1634567890123L,
    lastLoginTime = 1634567990456L
)

val jsonString = snakeCaseJson.encodeToString(account)
/* Output:
{
  "user_id": 123,
  "first_name": "Alice", 
  "last_name": "Smith",
  "email_address": "alice.smith@example.com",
  "is_active": true,
  "created_at": 1634567890123,
  "last_login_time": 1634567990456
}
*/

// Deserialization also uses snake_case
val snakeCaseInput = """
{
  "user_id": 456,
  "first_name": "Bob",
  "last_name": "Johnson",
  "email_address": "bob@example.com",
  "is_active": false,
  "created_at": 1634567800000,
  "last_login_time": null
}
"""

val deserializedAccount = snakeCaseJson.decodeFromString<UserAccount>(snakeCaseInput)
// UserAccount(userId=456, firstName="Bob", lastName="Johnson", ...)

// Acronym handling
@Serializable
data class APIConfiguration(
    val httpURL: String,
    val apiKey: String,
    val maxHTTPConnections: Int,
    val enableHTTP2: Boolean,
    val xmlHttpTimeout: Long
)

val apiConfig = APIConfiguration(
    httpURL = "https://api.example.com",
    apiKey = "secret123",
    maxHTTPConnections = 10,
    enableHTTP2 = true,
    xmlHttpTimeout = 30000L
)

val apiJson = snakeCaseJson.encodeToString(apiConfig)
/* Output:
{
  "http_url": "https://api.example.com",
  "api_key": "secret123",
  "max_http_connections": 10,
  "enable_http2": true,
  "xml_http_timeout": 30000
}
*/

KebabCase Strategy

Transforms camelCase property names to kebab-case format.

Usage Examples:

@Serializable
data class ServerConfig(
    val serverPort: Int,
    val bindAddress: String,
    val maxConnections: Int,
    val enableSSL: Boolean,
    val sslCertPath: String?,
    val requestTimeout: Long,
    val keepAliveTimeout: Long
)

// Configure Json with kebab-case naming
val kebabCaseJson = Json {
    namingStrategy = JsonNamingStrategy.KebabCase
    prettyPrint = true
}

val config = ServerConfig(
    serverPort = 8080,
    bindAddress = "0.0.0.0",
    maxConnections = 1000,
    enableSSL = true,
    sslCertPath = "/etc/ssl/cert.pem",
    requestTimeout = 30000L,
    keepAliveTimeout = 60000L
)

val configJson = kebabCaseJson.encodeToString(config)
/* Output:
{
  "server-port": 8080,
  "bind-address": "0.0.0.0",
  "max-connections": 1000,
  "enable-ssl": true,
  "ssl-cert-path": "/etc/ssl/cert.pem",
  "request-timeout": 30000,
  "keep-alive-timeout": 60000
}
*/

// Works with deserialization too
val kebabInput = """
{
  "server-port": 9090,
  "bind-address": "127.0.0.1", 
  "max-connections": 500,
  "enable-ssl": false,
  "ssl-cert-path": null,
  "request-timeout": 15000,
  "keep-alive-timeout": 30000
}
"""

val deserializedConfig = kebabCaseJson.decodeFromString<ServerConfig>(kebabInput)

// CSS-like configuration format
@Serializable  
data class StyleConfig(
    val backgroundColor: String,
    val textColor: String,
    val fontSize: Int,
    val fontWeight: String,
    val borderRadius: Int,
    val marginTop: Int,
    val paddingLeft: Int
)

val style = StyleConfig(
    backgroundColor = "#ffffff",
    textColor = "#333333",
    fontSize = 14,
    fontWeight = "bold",
    borderRadius = 4,
    marginTop = 10,
    paddingLeft = 20
)

val styleJson = kebabCaseJson.encodeToString(style)
/* Output:
{
  "background-color": "#ffffff",
  "text-color": "#333333", 
  "font-size": 14,
  "font-weight": "bold",
  "border-radius": 4,
  "margin-top": 10,
  "padding-left": 20
}
*/

Custom Naming Strategies

Implementing custom naming transformation logic.

Usage Examples:

// Custom strategy for API field names
val apiNamingStrategy = JsonNamingStrategy { descriptor, elementIndex, serialName ->
    when {
        // ID fields get special treatment
        serialName.endsWith("Id") -> serialName.removeSuffix("Id") + "_id"
        serialName.endsWith("ID") -> serialName.removeSuffix("ID") + "_id"
        
        // Boolean fields get is_ prefix
        descriptor.getElementDescriptor(elementIndex).kind == PrimitiveKind.BOOLEAN &&
        !serialName.startsWith("is") -> "is_$serialName"
        
        // Everything else to snake_case
        else -> serialName.fold("") { acc, char ->
            when {
                char.isUpperCase() && acc.isNotEmpty() -> acc + "_" + char.lowercase()
                else -> acc + char.lowercase()
            }
        }
    }
}

@Serializable
data class EntityModel(
    val entityId: Long,
    val parentID: Long?,
    val name: String,
    val active: Boolean,
    val verified: Boolean,
    val createdAt: Long
)

val customJson = Json { 
    namingStrategy = apiNamingStrategy
    prettyPrint = true
}

val entity = EntityModel(
    entityId = 123L,
    parentID = 456L, 
    name = "Test Entity",
    active = true,
    verified = false,
    createdAt = System.currentTimeMillis()
)

val entityJson = customJson.encodeToString(entity)
/* Output:
{
  "entity_id": 123,
  "parent_id": 456,
  "name": "Test Entity",
  "is_active": true,
  "is_verified": false,
  "created_at": 1634567890123
}
*/

// Context-aware naming strategy
val contextualNamingStrategy = JsonNamingStrategy { descriptor, elementIndex, serialName ->
    val className = descriptor.serialName.substringAfterLast(".")
    
    when (className) {
        "DatabaseConfig" -> {
            // Database configs use underscore convention
            serialName.fold("") { acc, char ->
                if (char.isUpperCase() && acc.isNotEmpty()) acc + "_" + char.lowercase()
                else acc + char.lowercase()
            }
        }
        "UIComponent" -> {
            // UI components use kebab-case
            serialName.fold("") { acc, char ->
                if (char.isUpperCase() && acc.isNotEmpty()) acc + "-" + char.lowercase()
                else acc + char.lowercase()
            }
        }
        else -> serialName // No transformation for other classes
    }
}

@Serializable
data class DatabaseConfig(
    val hostName: String,
    val portNumber: Int,
    val databaseName: String,
    val connectionTimeout: Long
)

@Serializable 
data class UIComponent(
    val componentType: String,
    val backgroundColor: String,
    val borderWidth: Int,
    val isVisible: Boolean
)

@Serializable
data class RegularClass(
    val someProperty: String,
    val anotherProperty: Int
)

val contextualJson = Json { namingStrategy = contextualNamingStrategy }

val dbConfig = DatabaseConfig("localhost", 5432, "myapp", 30000L)
val uiComponent = UIComponent("button", "#ffffff", 1, true)
val regular = RegularClass("value", 42)

// Each class gets appropriate naming convention
val dbJson = contextualJson.encodeToString(dbConfig)
// {"host_name":"localhost","port_number":5432,"database_name":"myapp","connection_timeout":30000}

val uiJson = contextualJson.encodeToString(uiComponent)  
// {"component-type":"button","background-color":"#ffffff","border-width":1,"is-visible":true}

val regularJson = contextualJson.encodeToString(regular)
// {"someProperty":"value","anotherProperty":42}

Naming Strategy Considerations

Important considerations when using naming strategies.

Usage Examples:

// Naming strategies affect ALL properties
@Serializable
data class MixedCase(
    @SerialName("custom_name") // SerialName is also transformed!
    val property1: String,
    val camelCaseProperty: String
)

val snakeJson = Json { namingStrategy = JsonNamingStrategy.SnakeCase }
val mixed = MixedCase("value1", "value2")
val result = snakeJson.encodeToString(mixed)
// {"custom_name":"value1","camel_case_property":"value2"} 
// Note: custom_name becomes custom_name (already snake_case)

// Use JsonNames for alternative deserialization
@Serializable
data class FlexibleModel(
    @JsonNames("legacy_name", "old_name") // JsonNames values are NOT transformed
    val newName: String
)

val flexibleJson = Json { namingStrategy = JsonNamingStrategy.SnakeCase }

// Can deserialize from multiple formats
val input1 = """{"new_name":"value"}"""        // Transformed property name
val input2 = """{"legacy_name":"value"}"""     // JsonNames alternative (not transformed)
val input3 = """{"old_name":"value"}"""        // Another JsonNames alternative

val result1 = flexibleJson.decodeFromString<FlexibleModel>(input1)
val result2 = flexibleJson.decodeFromString<FlexibleModel>(input2)  
val result3 = flexibleJson.decodeFromString<FlexibleModel>(input3)
// All create FlexibleModel(newName="value")

// But encoding uses transformed name
val encoded = flexibleJson.encodeToString(result1)
// {"new_name":"value"}

// Polymorphic serialization is not affected
@Serializable
@JsonClassDiscriminator("messageType") // NOT transformed
sealed class Message {
    @Serializable
    data class TextMessage(val messageText: String) : Message() // messageText -> message_text
}

val polyJson = Json { 
    namingStrategy = JsonNamingStrategy.SnakeCase
    serializersModule = SerializersModule {
        polymorphic(Message::class) {
            subclass(Message.TextMessage::class)
        }
    }
}

val textMsg: Message = Message.TextMessage("Hello")
val polyResult = polyJson.encodeToString(textMsg)
// {"messageType":"TextMessage","message_text":"Hello"}
// Note: discriminator key is NOT transformed, but properties are

Install with Tessl CLI

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

docs

annotations.md

configuration.md

core-operations.md

custom-serializers.md

dsl-builders.md

index.md

json-elements.md

naming-strategies.md

platform-extensions.md

tile.json