Kotlin Help

Customize the Json instance

The default Json instance strictly follows the JSON specification and the declarations in Kotlin classes.

You can use more flexible JSON features or type conversions by creating a custom Json instance with the Json() builder function:

// Creates a Json instance based on the default configuration, allowing special floating-point values val customJson = Json { allowSpecialFloatingPointValues = true } // Use the customJson instance with the same syntax as the default one to encode a string val jsonString = customJson.encodeToString(Data(Double.NaN)) println(jsonString)

Json instances created this way are immutable and thread-safe, which makes them safe to store in a top-level property for reuse.

You can also base a new Json instance on an existing one and modify its settings using the same builder syntax:

// Creates a new instance based on an existing Json val lenientJson = Json(customJson) { isLenient = true prettyPrint = true }

Customize JSON structure

You can customize how a Json instance structures data during encoding and decoding. This allows you to control which values appear in the output and how specific types are represented.

Encode default values

By default, the JSON encoder omits default property values because they are automatically applied to missing properties during decoding. This behavior is especially useful for nullable properties with null defaults, as it avoids writing unnecessary null values. For more details, see the Manage serialization of default properties section.

To change this default behavior, set the encodeDefaults property to true in a Json instance:

// Imports declarations from the serialization library import kotlinx.serialization.* import kotlinx.serialization.json.* //sampleStart // Configures a Json instance to encode default values val format = Json { encodeDefaults = true } @Serializable class Project( val name: String, val language: String = "Kotlin", val website: String? = null ) fun main() { val data = Project("kotlinx.serialization") // Encodes all the property values, including the default ones println(format.encodeToString(data)) // {"name":"kotlinx.serialization","language":"Kotlin","website":null} } //sampleEnd

Omit explicit nulls

By default, all null values are encoded into JSON output. To omit null values, set the explicitNulls property to false in a Json instance:

// Imports declarations from the serialization library import kotlinx.serialization.* import kotlinx.serialization.json.* //sampleStart // Configures a Json instance to omit null values during serialization val format = Json { explicitNulls = false } @Serializable data class Project( val name: String, val language: String, val version: String? = "1.2.2", val website: String?, val description: String? = null ) fun main() { val data = Project("kotlinx.serialization", "Kotlin", null, null, null) val json = format.encodeToString(data) // Omits version, website, and description properties from the JSON output println(json) // {"name":"kotlinx.serialization","language":"Kotlin"} // Treats missing nullable properties without defaults as null // Fills properties that have defaults with their default values println(format.decodeFromString<Project>(json)) // Project(name=kotlinx.serialization, language=Kotlin, version=1.2.2, website=null, description=null) } //sampleEnd

When explicitNulls is set to false, encoding and decoding can become asymmetrical. In this example, the version property is null before encoding, but it decodes to 1.2.2.

Allow structured map keys

The JSON format doesn't natively support maps with structured keys, because JSON keys are strings that represent only primitives or enums. To serialize and deserialize maps with user-defined class keys, use the allowStructuredMapKeys property:

// Imports declarations from the serialization library import kotlinx.serialization.* import kotlinx.serialization.json.* //sampleStart // Configures a Json instance to encode maps with structured keys val format = Json { allowStructuredMapKeys = true } @Serializable data class Project(val name: String) fun main() { val map = mapOf( Project("kotlinx.serialization") to "Serialization", Project("kotlinx.coroutines") to "Coroutines" ) // Serializes the map with structured keys as a JSON array: // [key1, value1, key2, value2,...] println(format.encodeToString(map)) // [{"name":"kotlinx.serialization"},"Serialization",{"name":"kotlinx.coroutines"},"Coroutines"] } //sampleEnd

Allow special floating-point values

By default, special floating-point values like Double.NaN and infinities aren't supported in JSON because the JSON specification prohibits them.

To enable their encoding and decoding, set the allowSpecialFloatingPointValues property to true in a Json instance:

// Imports declarations from the serialization library import kotlinx.serialization.* import kotlinx.serialization.json.* //sampleStart // Configures a Json instance to allow special floating-point values val format = Json { allowSpecialFloatingPointValues = true } @Serializable class Data( val value: Double ) fun main() { val data = Data(Double.NaN) // Produces a non-standard JSON output used for representing special floating-point values println(format.encodeToString(data)) // {"value":NaN} } //sampleEnd

Specify class discriminator for polymorphism

When working with polymorphic data, you can use the classDiscriminator property to specify a key name that identifies the type of the serialized polymorphic object. Combined with an explicit serial name defined with the @SerialName annotation, this approach gives you full control over the resulting JSON structure:

// Imports declarations from the serialization library import kotlinx.serialization.* import kotlinx.serialization.json.* //sampleStart // Configures a Json instance to use a custom class discriminator val format = Json { classDiscriminator = "#class" } @Serializable sealed class Project { abstract val name: String } // Specifies a custom serial name for the OwnedProject class @Serializable @SerialName("owned") class OwnedProject(override val name: String, val owner: String) : Project() // Specifies a custom serial name for the SimpleProject class @Serializable @SerialName("simple") class SimpleProject(override val name: String) : Project() fun main() { val simpleProject: Project = SimpleProject("kotlinx.serialization") val ownedProject: Project = OwnedProject("kotlinx.coroutines", "kotlin") // Serializes SimpleProject with #class: "simple" println(format.encodeToString(simpleProject)) // {"#class":"simple","name":"kotlinx.serialization"} // Serializes OwnedProject with #class: "owned" println(format.encodeToString(ownedProject)) // {"#class":"owned","name":"kotlinx.coroutines","owner":"kotlin"} } //sampleEnd

While the classDiscriminator property in a Json instance lets you specify a single discriminator key for all polymorphic types, the Experimental @JsonClassDiscriminator annotation offers more flexibility. It allows you to define a custom discriminator directly on the base class, which is automatically inherited by all its subclasses.

Here's an example:

// Imports declarations from the serialization library import kotlinx.serialization.* import kotlinx.serialization.json.* //sampleStart // The @JsonClassDiscriminator annotation is inheritable, so all subclasses of Base will have the same discriminator @Serializable @OptIn(ExperimentalSerializationApi::class) @JsonClassDiscriminator("message_type") sealed class Base // Inherits the discriminator from Base @Serializable sealed class ErrorClass: Base() // Defines a class that combines a message and an optional error @Serializable data class Message(val message: Base, val error: ErrorClass?) @Serializable @SerialName("my.app.BaseMessage") data class BaseMessage(val message: String) : Base() @Serializable @SerialName("my.app.GenericError") data class GenericError(@SerialName("error_code") val errorCode: Int) : ErrorClass() val format = Json { classDiscriminator = "#class" } fun main() { val data = Message(BaseMessage("not found"), GenericError(404)) // Uses the discriminator from Base for all subclasses println(format.encodeToString(data)) // {"message":{"message_type":"my.app.BaseMessage","message":"not found"},"error":{"message_type":"my.app.GenericError","error_code":404}} } //sampleEnd

Set class discriminator output mode

Use the JsonBuilder.classDiscriminatorMode property to control how class discriminators are added to your JSON output. By default, the discriminator is only added for polymorphic types, which is useful when working with polymorphic class hierarchies.

To adjust this behavior, set the ClassDiscriminatorMode property to one of these options:

  • POLYMORPHIC: (default) adds the class discriminator only for polymorphic types.

  • ALL_JSON_OBJECTS: adds the class discriminator to all JSON objects, wherever possible.

  • NONE: omits the class discriminator entirely.

Here's an example with the ClassDiscriminatorMode property set to NONE:

// Imports declarations from the serialization library import kotlinx.serialization.* import kotlinx.serialization.json.* //sampleStart // Configures a Json instance to omit the class discriminator from the output val format = Json { classDiscriminatorMode = ClassDiscriminatorMode.NONE } @Serializable sealed class Project { abstract val name: String } @Serializable class OwnedProject(override val name: String, val owner: String) : Project() fun main() { val data: Project = OwnedProject("kotlinx.coroutines", "kotlin") // Serializes without a discriminator println(format.encodeToString(data)) // {"name":"kotlinx.coroutines","owner":"kotlin"} } //sampleEnd

Pretty printing

By default, Json produces a compact, single-line output.

You can add indentations and line breaks for better readability by enabling pretty printing in the output. To do so, set the prettyPrint property to true in a Json instance:

// Imports declarations from the serialization library import kotlinx.serialization.* import kotlinx.serialization.json.* //sampleStart // Creates a custom Json format val format = Json { prettyPrint = true } @Serializable data class Project(val name: String, val language: String) fun main() { val data = Project("kotlinx.serialization", "Kotlin") // Prints the JSON output with line breaks and indentations println(format.encodeToString(data)) } //sampleEnd

This example prints the following result:

{ "name": "kotlinx.serialization", "language": "Kotlin" }

Customize JSON deserialization

Kotlin's Json parser offers several settings that allow you to customize the parsing and deserialization of JSON data.

Ignore unknown keys

When working with JSON data from third-party services or other dynamic sources, new properties may be added to the JSON objects over time.

By default, unknown keys (the property names in the JSON input) result in an error during deserialization. To prevent this, set the ignoreUnknownKeys property to true in a Json instance:

// Imports declarations from the serialization library import kotlinx.serialization.* import kotlinx.serialization.json.* //sampleStart // Creates a Json instance to ignore unknown keys val format = Json { ignoreUnknownKeys = true } @Serializable data class Project(val name: String) fun main() { val data = format.decodeFromString<Project>(""" {"name":"kotlinx.serialization","language":"Kotlin"} """) // The language key is ignored because it's not in the Project class println(data) // Project(name=kotlinx.serialization) } //sampleEnd

Ignore unknown keys for specific classes

Instead of enabling ignoreUnknownKeys for all classes, you can ignore unknown keys only for specific classes by using the @JsonIgnoreUnknownKeys annotation. This allows you to keep strict deserialization by default while allowing leniency only where needed.

The @JsonIgnoreUnknownKeys annotation is Experimental. To opt in, use the @OptIn(ExperimentalSerializationApi::class) annotation or the compiler option -opt-in=kotlinx.serialization.ExperimentalSerializationApi:

// Imports declarations from the serialization library import kotlinx.serialization.* import kotlinx.serialization.json.* //sampleStart @OptIn(ExperimentalSerializationApi::class) @Serializable // Unknown properties in Outer are ignored during deserialization @JsonIgnoreUnknownKeys data class Outer(val a: Int, val inner: Inner) @Serializable data class Inner(val x: String) fun main() { val outer = Json.decodeFromString<Outer>( """{"a":1,"inner":{"x":"value"},"unknownKey":42}""" ) println(outer) // Outer(a=1, inner=Inner(x=value)) // Throws an exception // unknownKey inside inner is NOT ignored because Inner is not annotated println( Json.decodeFromString<Outer>( """{"a":1,"inner":{"x":"value","unknownKey":"unexpected"}}""" ) ) } //sampleEnd

In this example, Inner throws a SerializationException for unknown keys because it isn't annotated with @JsonIgnoreUnknownKeys.

Coerce input values

When working with JSON data from third-party services or other dynamic sources, the format can evolve. This may cause exceptions during decoding when actual values don't match the expected types.

The default Json implementation is strict about input types. To relax this restriction, set the coerceInputValues property to true in a Json instance:

// Imports declarations from the serialization library import kotlinx.serialization.* import kotlinx.serialization.json.* //sampleStart val format = Json { coerceInputValues = true } @Serializable data class Project(val name: String, val language: String = "Kotlin") fun main() { val data = format.decodeFromString<Project>(""" {"name":"kotlinx.serialization","language":null} """) // Coerces the invalid null value for language to its default value println(data) // Project(name=kotlinx.serialization, language=Kotlin) } //sampleEnd

The coerceInputValues property only affects decoding. It treats certain invalid input values as if the corresponding property were missing. Currently, it applies to:

  • null inputs for non-nullable types

  • unknown values for enums

If a value is missing, it's replaced with a default property value if one exists.

For enums, the value is replaced with null only if:

  • No default is defined.

  • The explicitNulls property is set to false.

  • The property is nullable.

You can combine coerceInputValues with the explicitNulls property to handle invalid enum values:

// Imports declarations from the serialization library import kotlinx.serialization.* import kotlinx.serialization.json.* //sampleStart enum class Color { BLACK, WHITE } @Serializable data class Brush(val foreground: Color = Color.BLACK, val background: Color?) val json = Json { coerceInputValues = true explicitNulls = false } fun main() { // Coerces the unknown foreground value to its default and background to null val brush = json.decodeFromString<Brush>("""{"foreground":"pink", "background":"purple"}""") println(brush) // Brush(foreground=BLACK, background=null) } //sampleEnd

Allow trailing commas

To allow trailing commas in JSON input, set the allowTrailingComma property to true:

// Imports declarations from the serialization library import kotlinx.serialization.* import kotlinx.serialization.json.* //sampleStart // Allows trailing commas in JSON objects and arrays val format = Json { allowTrailingComma = true } fun main() { val numbers = format.decodeFromString<List<Int>>( """ [1, 2, 3,] """ ) println(numbers) // [1, 2, 3] } //sampleEnd

Allow comments in JSON

Use the allowComments property to allow comments in JSON input. When this property is enabled, the parser accepts the following comment forms in the input:

  • // line comments that end at a newline \n

  • /* */ block comments

Here's an example:

// Imports declarations from the serialization library import kotlinx.serialization.* import kotlinx.serialization.json.* //sampleStart // Allows comments in JSON input val format = Json { allowComments = true } fun main() { val numbers = format.decodeFromString<List<Int>>( """ [ // first element 1, /* second element */ 2 ] """ ) println(numbers) // [1, 2] } //sampleEnd

Lenient parsing

By default, the Json parser enforces strict JSON rules to ensure compliance with the RFC-8259 specification, which requires keys and string literals to be quoted.

To relax these restrictions, set the isLenient property to true in a Json instance:

// Imports declarations from the serialization library import kotlinx.serialization.* import kotlinx.serialization.json.* //sampleStart val format = Json { isLenient = true } enum class Status { SUPPORTED } @Serializable data class Project(val name: String, val status: Status, val votes: Int) fun main() { // Decodes a JSON string with lenient parsing // Lenient parsing allows unquoted keys, string, and enum values val data = format.decodeFromString<Project>(""" { name : kotlinx.serialization, status : SUPPORTED, votes : "9000" } """) println(data) // Project(name=kotlinx.serialization, status=SUPPORTED, votes=9000) } //sampleEnd

Customize name mapping between JSON and Kotlin

Some JSON data may not perfectly align with Kotlin's naming conventions or expected formats. To address these challenges, the Kotlin serialization library provides several tools to manage naming discrepancies, handle multiple JSON property names, and ensure consistent naming strategies across serialized data.

Accept alternative JSON property names for a single Kotlin property

When JSON property names change between schema versions, you can use the @SerialName annotation to rename a JSON property.

However, this prevents decoding data that still uses previous property names. To accept alternative JSON property names for a single property, use the @JsonNames annotation:

// Imports declarations from the serialization library import kotlinx.serialization.* import kotlinx.serialization.json.* //sampleStart @Serializable // Maps both name and title JSON properties to the name property data class Project(@JsonNames("title") val name: String) fun main() { val project = Json.decodeFromString<Project>("""{"name":"kotlinx.serialization"}""") println(project) // Project(name=kotlinx.serialization) val oldProject = Json.decodeFromString<Project>("""{"title":"kotlinx.coroutines"}""") // Both name and title Json properties correspond to name property println(oldProject) // Project(name=kotlinx.coroutines) } //sampleEnd

Decode enums in a case-insensitive manner

Kotlin's naming conventions recommend naming enum values in uppercase with underscores or in upper camel case. By default, Json uses the exact names of Kotlin enum constants when decoding.

However, JSON data from external sources might use lowercase or mixed-case names. To handle such cases, configure the Json instance to decode enum values case-insensitively with the JsonBuilder.decodeEnumsCaseInsensitive property:

// Imports declarations from the serialization library import kotlinx.serialization.* import kotlinx.serialization.json.* //sampleStart // Configures a Json instance to decode enum values in a case-insensitive way val format = Json { decodeEnumsCaseInsensitive = true } enum class Cases { VALUE_A, @JsonNames("Alternative") VALUE_B } @Serializable data class CasesList(val cases: List<Cases>) fun main() { // Decodes enum values regardless of their case, including alternative names println(format.decodeFromString<CasesList>("""{"cases":["value_A", "alternative"]}""")) // CasesList(cases=[VALUE_A, VALUE_B]) } //sampleEnd

Apply a global naming strategy

When property names in JSON input differ from those in Kotlin, you can specify the name of each property explicitly using the @SerialName annotation. However, when migrating from other frameworks or a legacy codebase, you might need to transform every serial name in the same way.

For these scenarios, you can specify a global naming strategy using the JsonBuilder.namingStrategy property in a Json instance. The Kotlin serialization library provides built-in strategies, such as JsonNamingStrategy.SnakeCase:

// Imports declarations from the serialization library import kotlinx.serialization.* import kotlinx.serialization.json.* //sampleStart @Serializable data class Project(val projectName: String, val projectOwner: String) // Configures a Json instance to apply SnakeCase naming strategy val format = Json { namingStrategy = JsonNamingStrategy.SnakeCase } fun main() { val project = format.decodeFromString<Project>("""{"project_name":"kotlinx.coroutines", "project_owner":"Kotlin"}""") // Serializes and deserializes as if all serial names are transformed from camel case to snake case println(format.encodeToString(project.copy(projectName = "kotlinx.serialization"))) // {"project_name":"kotlinx.serialization","project_owner":"Kotlin"} } //sampleEnd

When using a global naming strategy with JsonNamingStrategy, keep the following in mind:

  • The transformation applies to all properties, whether the serial name is derived from the property name or explicitly defined with the @SerialName annotation. You can't exclude a property from transformation by specifying a serial name. To keep specific names unchanged during serialization, use the @JsonNames annotation instead.

  • If a transformed name conflicts with other transformed property names or with alternative names specified by the @JsonNames annotation, deserialization fails with an exception.

  • Global naming strategies are implicit. It makes it difficult to determine the serialized names by looking at the class definition. This can make tasks like Find Usages, and Rename in IDEs, or full-text searches using tools like grep, more difficult, potentially increasing the risk of bugs and maintenance costs.

Given these factors, consider the trade-offs carefully before implementing global naming strategies in your application.

What's next

15 January 2026