skip to content

Beyond @Serializable, which annotations does the serialization compiler plugin act on, and how do @SerialName, default values, and @Transient change the generated code?

level: seniorimportance: should knowfreq 40%

answer

  1. @SerialName renames in the descriptor
  2. @Transient drops field, needs a default
  3. default value => optional in descriptor
  4. @Required forces presence on decode
  5. plugin = structure; Json{} = runtime policy (encodeDefaults/ignoreUnknownKeys)

basics

~20 s

The plugin reads annotations like @SerialName (rename a field or class), @Transient (skip a field), and @Required, plus default values, and bakes those rules into the generated serializer and its descriptor so encoding/decoding follow them.

solid answer

~50 s

The plugin is annotation-driven. `@Serializable` triggers generation. `@SerialName("x")` changes the name written into the **SerialDescriptor** for a class or property, so JSON uses that name instead of the Kotlin identifier. A **property with a default value** becomes *optional* in the descriptor: the generated `deserialize` can skip it if absent, and by default `Json` omits it on encode unless `encodeDefaults = true`. `@Transient` excludes a property from the descriptor entirely — it isn't encoded or decoded and **must** have a default (since it won't be read back). `@Required` forces an otherwise-optional (defaulted) property to be present on decode. `@SerialInfo`-based custom annotations and format-specific ones (`@JsonNames` for alternate input names, `@EncodeDefault`) also feed the generated descriptor/serializer. All of this is decided at compile time and stored in the descriptor; the runtime `Json` config then interprets flags like `encodeDefaults`/`ignoreUnknownKeys`.

code

kotlin · 9 lines
kotlin
@Serializable
data class Config(
    @SerialName("timeout_ms") val timeoutMs: Int = 1000, // optional, renamed
    @Transient val handle: Any? = null,                  // skipped; needs default
    @Required val name: String = "unset"                 // must be present on decode
)

val j = Json { encodeDefaults = true; ignoreUnknownKeys = true }
j.encodeToString(Config()) // {"timeout_ms":1000,"name":"unset"}

go deeper

for a junior

Recognizes @SerialName renames a field and @Transient skips it.

for a middle

Explains how defaults make a property optional and how @Required/@Transient interact with defaults.

for a senior

Clearly separates what the plugin bakes into the descriptor from what the runtime Json config controls, and reasons about polymorphic discriminators.

for a principal

Designs wire-format contracts (naming, optionality, evolution) using these annotations and chooses Json policy defaults for backward/forward compatibility across services.

## The plugin is annotation-driven codegen The `kotlin("plugin.serialization")` plugin inspects compile-time annotations and embeds their effects into the generated `KSerializer` and especially its `SerialDescriptor`. ## Annotations the plugin honors - **`@Serializable`** — the trigger. On a class, generates the serializer; can also target a property/type with `@Serializable(with = CustomSerializer::class)` to override which serializer is used. - **`@SerialName("json_name")`** — renames a class (for polymorphic type discriminators) or a property. The new name goes into the descriptor, so output/lookup uses it instead of the Kotlin name. Lets you decouple wire format from Kotlin identifiers. - **`@Transient`** — excludes a property from serialization. It's dropped from the descriptor; it is neither encoded nor decoded, so it **must** declare a default value. - **`@Required`** — makes a property that has a default value still mandatory on decode (overriding the 'defaults are optional' rule). - **`@EncodeDefault(Mode.ALWAYS|NEVER)`** — per-property override of whether a defaulted value is written, independent of `Json { encodeDefaults }`. - **`@JsonNames("alt1", "alt2")`** — accept alternative input names on decode (requires `Json { useAlternativeNames = true }`, on by default). ## How defaults interact with codegen ```kotlin @Serializable data class Config( @SerialName("timeout_ms") val timeoutMs: Int = 1000, @Transient val cachedHandle: Any? = null, @Required val name: String = "unset" ) ``` - `timeoutMs` → descriptor element name `timeout_ms`, marked **optional** (has default) → may be absent on decode; omitted on encode unless `encodeDefaults=true`. - `cachedHandle` → **not in descriptor**; must have a default; ignored entirely. - `name` → optional-by-default because it has a default, but `@Required` forces it to be present on decode. ## Where compile-time ends and runtime begins The plugin records *structure and rules* in the descriptor. The **runtime `Json` instance** decides behavior toggles: `encodeDefaults`, `ignoreUnknownKeys`, `explicitNulls`, `coerceInputValues`, `classDiscriminator`. So the same generated serializer behaves differently under different `Json {}` configs — separation of concerns between codegen (the plugin) and policy (the runtime format). ## Polymorphism note `@SerialName` on sealed subclasses sets the value of the `classDiscriminator` (default `"type"`) used to pick the subtype on decode.

  • Why must a @Transient property have a default value?
    Because it's excluded from decoding, the generated deserialize never reads it from input. The constructor still needs a value, so it must come from a default.
  • If a property has a default, when does it actually appear in the JSON output?
    By default it's omitted on encode. It appears if Json has encodeDefaults=true, or if the property is annotated @EncodeDefault(ALWAYS).

saying these in an interview costs you the question

  • Thinking @Transient is the same as Java's transient keyword semantics in all formats
  • Believing a property with a default is always written to JSON
  • Confusing compile-time descriptor rules with runtime Json toggles
  • Not knowing @SerialName affects polymorphic discriminator values
  • Saying @Transient fields can be required

context