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?
answer
- encodeDefaults = false by default -> omits values equal to default
- Mode.ALWAYS = always emit, even at default
- Mode.NEVER = always omit, even if global encodeDefaults=true
- @EncodeDefault overrides the global flag per property
- ALWAYS bakes the current default into stored data
basics
~10 sUse @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 sWith 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@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 consumersgo deeper
Aware defaults are sometimes omitted from JSON output.
Knows encodeDefaults = false omits values equal to the default and that the flag can be toggled.
Explains ALWAYS vs NEVER, that they override the global flag per property, and the size-vs-self-describing tradeoff.
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