Kotlin JSON Serialization and Validation

Configure generated serializers, decode typed JSON models, and separate compatibility choices from domain validation.

Lesson 39 of 43Advanced9 min read

JSON transports values such as strings, numbers, booleans, arrays and objects. Kotlin models add named properties and types. Serialization converts a model into an external representation; deserialization reconstructs a model from input. The kotlinx.serialization library supplies JSON support and uses a compiler plugin to generate serializers for annotated classes.

Start with the data classes lesson. This guide teaches the additional contract required when values cross a storage or network boundary. A successful decode proves that data fits the configured serializer's rules. It does not prove that a customer is authorized, a price is acceptable, or the request is safe to execute.

Configure the plugin and the runtime library

Kotlin/JVM compilation needs the serialization compiler plugin as well as the JSON runtime dependency. The plugin version follows the Kotlin compiler version; the kotlinx.serialization runtime has its own independent version. Adding an import alone does not install either requirement.

For a pinned teaching project, use Kotlin JVM and serialization plugins 2.2.20, Gradle 8.14.3, JDK 17 and kotlinx-serialization-json 1.9.0. The 1.9.0 release requires Kotlin 2.2. These are explicit compatible example versions, not a claim that every future project should keep them indefinitely. Review official compatibility notes when upgrading.

Apply kotlin("plugin.serialization") version "2.2.20" beside the JVM plugin. Add implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0") and mavenCentral as the repository. Mark models with kotlinx.serialization.Serializable, which is distinct from Java's marker interface with a similar name. The model can then participate in generated typed encoding and decoding.

Missing values, null and unknown keys

A missing property and an explicit null are different inputs. A non-null String property cannot accept null merely because another property has a default. A nullable property without a default is not automatically optional in the serialized contract; use an explicit default when absence is intended to be allowed.

Unknown keys are rejected by the default JSON configuration. IgnoreUnknownKeys can allow a reader to tolerate additional fields from a newer producer. That can be useful for backward compatibility, but it should be a conscious contract choice. Ignoring a misspelled required field does not synthesize its intended value, and relaxed parsing does not validate business meaning.

SerialName can decouple a Kotlin property name from the external wire name. This helps preserve an API contract during an internal rename. Avoid casually changing serialized names in stored documents or shared network formats. Consumers may rely on those names even if your own application compiles after the change.

Validate after decoding and report useful failures

Use a transport model when the external shape differs from your validated domain model. Decode into the transport type, check ranges and required text, then construct the domain value. This separates malformed JSON from syntactically valid but unacceptable data and makes validation tests easier to write.

Catch expected serialization failures near the boundary when the application should return a friendly rejection. Avoid blanket handling that hides unrelated programming defects. Construction-time require checks can raise IllegalArgumentException, so account for your own validation design rather than assuming every rejected model produces only one serialization exception type.

Treat logs as another data boundary. Full JSON payloads may contain personal information or credentials. Record a useful field or error category without dumping secrets. Size-limit untrusted payloads before parsing them, and apply authentication and authorization separately before using decoded values to update records or invoke external services.

Official references

The Kotlin serialization overview explains library and plugin responsibilities. The project's basic serialization guide documents defaults and nullability, and the 1.9.0 release notes describe its compiler requirement. Build a fixture set covering missing fields, extra fields, nulls, wrong types and domain boundaries.

Kotlin JSON Serialization and Validation Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Encode and decode a stock item

The following complete Main.kt runs in a JVM application configured with the dependencies above. The explicit encodeDefaults option makes the published JSON policy visible rather than relying on a reader's assumptions about omitted fields.

Active has a constructor default, so missing active input can use true. Code and quantity have no defaults and are required for this model. Property ordering in this example follows the generated serializer, but applications should compare parsed values rather than require arbitrary incoming JSON key order.

Kotlin
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json

@Serializable
data class StockItem(val code: String, val quantity: Int, val active: Boolean = true)

fun main() {
    val json = Json { encodeDefaults = true }
    val text = json.encodeToString(StockItem("PEN", 3))
    println(text)
    val item = json.decodeFromString<StockItem>("""{"code":"BOOK","quantity":2}""")
    println("${item.code}:${item.quantity}:${item.active}")
}
Output
{"code":"PEN","quantity":3,"active":true}
BOOK:2:true

Kotlin JSON Serialization and Validation in Real Projects

Worked exercise: Decode a validated reorder request

Accept a request with a nonblank code and quantity from one through fifty. Allow extra fields for this explicitly tolerant input contract. Return null for malformed input or failed validation, and never perform a stock update inside the parser.

Kotlin
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json

@Serializable
data class ReorderInput(val code: String, val quantity: Int)
data class Reorder(val code: String, val quantity: Int)
private val requestJson = Json { ignoreUnknownKeys = true }

fun parseReorder(text: String): Reorder? {
    val input = try {
        requestJson.decodeFromString<ReorderInput>(text)
    } catch (error: SerializationException) {
        return null
    }
    val code = input.code.trim()
    if (code.isEmpty() || input.quantity !in 1..50) return null
    return Reorder(code, input.quantity)
}

fun main() {
    println(parseReorder("""{"code":" PEN ","quantity":4,"note":"rush"}"""))
    println(parseReorder("""{"code":"PEN","quantity":0}"""))
    println(parseReorder("not json"))
}
Output
Reorder(code=PEN, quantity=4)
null
null

The worked parser uses no throwing init validation of its own, so SerializationException covers its expected decode failures. A real API may return a structured error instead of null to distinguish invalid syntax from an invalid quantity. Authorization still belongs to the operation that later consumes the validated request.

Kotlin JSON Serialization and Validation FAQ

Does data class automatically make a model serializable?

No. Data classes provide language-generated equality, copying and related members, but kotlinx.serialization needs its own Serializable annotation and compiler plugin for generated model serializers. The runtime JSON library is a separate dependency. A class can be serializable without being a data class, and a data class can exist entirely without JSON support. Keep these responsibilities distinct when diagnosing missing serializers or unresolved imports in a new project.

Is a nullable property optional in incoming JSON?

Not necessarily. Nullability permits an explicit null value; a default determines whether a missing property can use a fallback in the ordinary generated contract. A nullable property with no default can still be required. If absence should mean null, declare an explicit null default and document that policy. Test missing, explicit null and a real value separately, because external producers may send each form for different reasons.

Should every decoder enable ignoreUnknownKeys?

Choose based on the protocol. Tolerance is useful when readers must accept additional fields introduced by newer producers. Strictness can catch misspellings and unexpected payloads in a tightly controlled format. Neither choice is universally correct. Document the policy and test versioned inputs, especially when ignoring an unfamiliar field might conceal a misunderstood request. Required known fields and domain validation remain necessary even when unknown fields are intentionally ignored.

Can successful decoding replace security validation?

No. Decoding checks representation and typed structure according to the serializer. It does not prove ownership, permission, inventory availability or a safe file destination. Validate domain constraints after decoding and authorize the action before performing it. Keep secrets out of payload logs, enforce sensible size limits, and avoid allowing arbitrary decoded class names or paths to choose privileged operations. Treat a decoded object as structured input, not automatically trusted input.

Kotlin JSON Serialization and Validation Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choicemedium

A nullable field should accept omission as null. Which declaration states that ordinary policy?

Kotlin JSON Serialization and Validation Quiz

2 questions to check what you learned.

  1. Which version normally matches the Kotlin compiler version?

    • The encoded customer's quantity.
    • Every JSON runtime release number.
    • The Kotlin serialization compiler plugin version.
  2. What does successful typed JSON decoding establish?

    • The input fits the configured representation and model contract.
    • The caller owns every referenced record.
    • Every business quantity is automatically valid.