Beyond @Serializable, which annotations does the serialization compiler plugin act on, and how do @SerialName, default values, and @Transient change the generated code?
answer
- @SerialName renames in the descriptor
- @Transient drops field, needs a default
- default value => optional in descriptor
- @Required forces presence on decode
- plugin = structure; Json{} = runtime policy (encodeDefaults/ignoreUnknownKeys)
basics
~20 sThe 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 sThe 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@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
Recognizes @SerialName renames a field and @Transient skips it.
Explains how defaults make a property optional and how @Required/@Transient interact with defaults.
Clearly separates what the plugin bakes into the descriptor from what the runtime Json config controls, and reasons about polymorphic discriminators.
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