skip to content

You receive a partner API: keys are snake_case, an 'apiVersion' must always be present in every payload you send, an internal 'requestTrace' must never leave your process, and a 'currency' field has a sensible default but must be present in inbound requests. Which kotlinx.serialization annotations/settings do you apply to each?

level: seniorimportance: should knowfreq 30%

answer

  1. Rename -> @SerialName / JsonNamingStrategy
  2. Always emit -> @EncodeDefault(ALWAYS)
  3. Never on wire -> @Transient (needs default)
  4. Mandatory inbound -> @Required (keeps default)
  5. Four independent axes: name / encode / presence / require

basics

~10 s

Rename keys with @SerialName (or a snake_case naming strategy); force apiVersion out with @EncodeDefault(ALWAYS); hide requestTrace with @Transient; make currency mandatory on input with @Required while keeping its default.

solid answer

~40 s

Map each requirement to the right tool. snake_case keys: per-field @SerialName("...") or globally Json { namingStrategy = JsonNamingStrategy.SnakeCase } (explicit @SerialName still wins per field). apiVersion always emitted: give it a default and annotate @EncodeDefault(Mode.ALWAYS) so it survives even with encodeDefaults=false. requestTrace never on the wire: @Transient (must give it a default; it's excluded from both encode and decode). currency has a default but is mandatory inbound: @Required so a missing key throws MissingFieldException on decode, while the default still serves in-code construction — and ensure it IS encoded (don't NEVER it) so round-tripping holds. This shows the four axes are independent: rename, encode-presence, wire-exclusion, decode-requirement.

code

kotlin · 7 lines
kotlin
@Serializable
data class Order(
    @SerialName("order_id") val orderId: String,
    @EncodeDefault(EncodeDefault.Mode.ALWAYS) val apiVersion: Int = 3,
    @Transient val requestTrace: String = "",
    @Required val currency: String = "USD",
)

go deeper

for a junior

Picks @SerialName for renaming and @Transient for hiding, with some guidance.

for a middle

Correctly maps three of four requirements and knows @Transient needs a default.

for a senior

Cleanly separates all four axes, justifies @EncodeDefault(ALWAYS) vs @Required, and avoids the NEVER+Required trap.

for a principal

Frames a model-wide convention (naming strategy + explicit overrides), guarantees encode/decode symmetry, and reasons about API versioning and PII exclusion across formats.

## Mapping requirements to annotations This question tests whether you can keep the **four orthogonal axes** straight. Each requirement maps to exactly one tool. ### 1. snake_case keys -> @SerialName (or naming strategy) ```kotlin @SerialName("request_id") val requestId: String ``` or centrally: ```kotlin val json = Json { namingStrategy = JsonNamingStrategy.SnakeCase } ``` The strategy converts every property name; an explicit `@SerialName` on a field overrides it. Strategy is JSON-only and applies to decode too. ### 2. apiVersion always present in output -> @EncodeDefault(Mode.ALWAYS) You want it emitted even under `encodeDefaults = false`: ```kotlin @EncodeDefault(EncodeDefault.Mode.ALWAYS) val apiVersion: Int = 3 ``` `ALWAYS` overrides the global omit-defaults setting, so the schema/version marker is never dropped. ### 3. requestTrace must never leave the process -> @Transient ```kotlin @Transient val requestTrace: String = newTrace() ``` `@Transient` removes it from the **descriptor**: never encoded, never decoded. It **must have a default** (the plugin enforces this). ### 4. currency: default in code, mandatory inbound -> @Required ```kotlin @Required val currency: String = "USD" ``` The default still works for `Order(...)` in Kotlin and tests, but decoding a payload **without** `currency` throws `MissingFieldException`. Make sure currency is actually **encoded** (don't combine with `Mode.NEVER`) so a message you produce can be read back. ## Putting it together ```kotlin import kotlinx.serialization.* import kotlinx.serialization.json.* @Serializable data class Order( @SerialName("order_id") val orderId: String, @EncodeDefault(EncodeDefault.Mode.ALWAYS) @SerialName("api_version") val apiVersion: Int = 3, @Transient val requestTrace: String = "", @Required @SerialName("currency") val currency: String = "USD", ) val json = Json { encodeDefaults = false } // compact, except ALWAYS-marked fields ``` ## The four independent axes (the real lesson) | Concern | Tool | |---|---| | Wire key name | `@SerialName` / `JsonNamingStrategy` | | Is a default written out? | `encodeDefaults` / `@EncodeDefault(ALWAYS\|NEVER)` | | Present on the wire at all? | `@Transient` (removes) | | May input omit it? | `@Required` (forbids omission) | ## Pitfalls - Don't reach for `@Transient` when you meant `@EncodeDefault(Mode.NEVER)`: NEVER still **decodes** the field if present, while `@Transient` removes it entirely. - Don't mark a field `@Required` and also `@EncodeDefault(Mode.NEVER)` — you'd emit payloads you can't re-decode. - A `@Transient` field can't simultaneously be `@Required` — there is no serial presence to require.

  • Why not use @EncodeDefault(Mode.NEVER) instead of @Transient for requestTrace?
    NEVER only skips it on encode but still decodes it if present in input; @Transient removes it from both encode and decode entirely.
  • Could you mark currency both @Required and @EncodeDefault(Mode.NEVER)?
    You could compile it, but it's a bug: you'd omit currency on encode yet require it on decode, so your own output can't be read back.

Like configuring a shipping label: rename the field (alias), force-print the tracking version, black out internal notes, and red-asterisk the customs field — four separate stamps, one per concern.

saying these in an interview costs you the question

  • Using @Transient when the field should still decode if supplied
  • Using @Required to try to force a field into the OUTPUT
  • Forgetting @Transient needs a default value
  • Combining @Required with Mode.NEVER and breaking round-trip
  • Thinking @SerialName affects encode-presence or requiredness

context