skip to content

By default kotlinx.serialization omits properties equal to their default during encoding. How do you control that with @EncodeDefault, and what are the Mode tradeoffs for schema evolution?

level: seniorimportance: should knowfreq 45%

answer

  1. encodeDefaults = false by default -> omits values equal to default
  2. Mode.ALWAYS = always emit, even at default
  3. Mode.NEVER = always omit, even if global encodeDefaults=true
  4. @EncodeDefault overrides the global flag per property
  5. ALWAYS bakes the current default into stored data

basics

~10 s

Use @EncodeDefault to force a property to be written even when it equals its default, or to never write it. Mode.ALWAYS always emits it; Mode.NEVER always omits it.

solid answer

~50 s

With the default `encodeDefaults = false`, kotlinx.serialization **omits** any property whose current value equals its declared default, producing leaner JSON. `@EncodeDefault(EncodeDefault.Mode.ALWAYS)` forces that property to always be serialized, even at its default — useful so downstream/strict consumers or non-Kotlin readers always see the field. `@EncodeDefault(EncodeDefault.Mode.NEVER)` forces omission even if `encodeDefaults = true` is set globally. The evolution tradeoff: omitting defaults keeps payloads small and lets you change a default later without rewriting old data, but a consumer that lacks the property's default (e.g. a different language, or a stricter validator) may break on the absent key. ALWAYS gives explicit, self-describing payloads at the cost of size and of 'baking in' the current default value into stored data. You can also flip the global behavior with `Json { encodeDefaults = true }`; `@EncodeDefault` overrides it per property.

code

kotlin · 8 lines
kotlin
@Serializable
data class Msg(
    val body: String,
    @EncodeDefault(EncodeDefault.Mode.ALWAYS) val schemaVersion: Int = 2
)

Json.encodeToString(Msg("hi"))
// {"body":"hi","schemaVersion":2}  -- version always present for consumers

go deeper

for a junior

Aware defaults are sometimes omitted from JSON output.

for a middle

Knows encodeDefaults = false omits values equal to the default and that the flag can be toggled.

for a senior

Explains ALWAYS vs NEVER, that they override the global flag per property, and the size-vs-self-describing tradeoff.

for a principal

Designs cross-language/contract policy: which discriminators must always emit, how baking defaults affects stored-data migration, and decode independence.

## Default encoding behavior kotlinx.serialization's `Json` ships with `encodeDefaults = false`. That means: **when a property's value equals its declared default, the key is left out of the output entirely.** This produces minimal JSON and relies on the *reader* to re-apply the default. ```kotlin @Serializable data class Settings(val theme: String = "dark", val volume: Int = 50) Json.encodeToString(Settings()) // -> {} (both at default, both omitted) Json.encodeToString(Settings(theme = "light")) // -> {"theme":"light"} ``` ## Global toggle `Json { encodeDefaults = true }` reverses this: every property is emitted, including ones at their default. ## Per-property control with @EncodeDefault The `@EncodeDefault` annotation overrides the global setting for a single property: - **`@EncodeDefault(EncodeDefault.Mode.ALWAYS)`** — always write this property, even when it equals its default, regardless of the global `encodeDefaults`. - **`@EncodeDefault(EncodeDefault.Mode.NEVER)`** — never write it (omit even when `encodeDefaults = true`). The reader must supply the default. ```kotlin @Serializable data class Event( val type: String, @EncodeDefault(EncodeDefault.Mode.ALWAYS) val version: Int = 1, // always emitted @EncodeDefault(EncodeDefault.Mode.NEVER) val debug: Boolean = false // never emitted ) ``` ## Evolution tradeoffs | Choice | Pros | Cons | |---|---|---| | Omit defaults (default) | Small payloads; you can change a default later without touching stored data | Consumers lacking the default break on the absent key | | `encodeDefaults = true` / `ALWAYS` | Self-describing, explicit, safe for strict/cross-language readers | Larger payloads; the current default value gets baked into stored data, so changing the default later won't retroactively update old records | | `NEVER` | Guarantees a field is hidden (e.g. internal/transient) even under a permissive global setting | Reader must know the default; absent key on a strict reader fails | ## When ALWAYS matters If a non-Kotlin consumer, a schema validator, or a strict `Json` (no defaults configured, no `ignoreUnknownKeys` interplay) reads your output, an omitted key may cause a `MissingFieldException` on *their* side. Forcing `ALWAYS` on critical discriminators (like a `version` field) makes payloads self-describing and robust across heterogeneous consumers. ## Key subtlety `@EncodeDefault` affects **encoding only**. It does not change decoding: a missing key on read still uses the property's default. So it never replaces defaults — it complements them.

  • Does @EncodeDefault change how missing keys decode?
    No. It only affects encoding. On decode, a missing key still falls back to the property's default.
  • Why might forcing ALWAYS on a default be risky for evolution?
    It bakes the current default value into stored payloads; later changing the default won't update those already-written records.

Omitting defaults is like not repeating 'standard shipping' on every order; @EncodeDefault(ALWAYS) is insisting it's printed on each receipt so any clerk reads it the same way.

saying these in an interview costs you the question

  • Saying defaults are always written out by default (they're omitted)
  • Claiming @EncodeDefault changes decoding behavior
  • Confusing Mode.NEVER with simply leaving encodeDefaults = false
  • Not recognizing that omitted defaults can break stricter/cross-language consumers
  • Thinking @EncodeDefault is needed to apply a default on read

context