Explain how a config DSL like Json { isLenient = true } works under the hood. What two Kotlin language features make this `{ ... }` block possible?
answer
- Last param is Builder.() -> Unit
- Trailing lambda moves outside parens
- Receiver means this == builder
- isLenient = true is this.isLenient = true
- Mutable builder -> immutable result
basics
~10 sThe braces are a function passed as the last argument. Inside, this is a settings object, so you set properties like isLenient directly without naming the object.
solid answer
~40 sJson { ... } is a function whose last parameter is a function type with receiver: `JsonBuilder.() -> Unit`. Two features combine. First, trailing-lambda syntax: when a lambda is the last argument it moves outside the parentheses, so `Json({...})` becomes `Json { ... }`. Second, the receiver type means inside the lambda `this` is a JsonBuilder instance, so `isLenient = true` is really `this.isLenient = true`. The Json function creates a JsonBuilder, applies your lambda to it to mutate its fields, then builds an immutable Json instance from the populated builder. This is the classic builder-via-receiver-lambda idiom shared by kotlinx.serialization, Ktor install blocks, and HTTP client config.
code
kotlin · 10 linesval json = Json {
isLenient = true
ignoreUnknownKeys = true
prettyPrint = false
}
// desugars to:
val json2 = Json(builderAction = fun JsonBuilder.() {
this.isLenient = true
this.ignoreUnknownKeys = true
})go deeper
Recognizes Json { } is a function call with a trailing lambda and that you set properties inside.
Explains function-type-with-receiver, the this rebinding, and the build-then-freeze flow.
Articulates why builder/result are split (mutability, thread-safety) and relates it to apply.
Discusses API design tradeoffs: validation timing, immutability guarantees, and forward-compatible config evolution.
## The shape of a config DSL When you write `Json { isLenient = true; ignoreUnknownKeys = true }`, you are calling a normal top-level function named `Json`. Its signature looks like: ```kotlin fun Json(builderAction: JsonBuilder.() -> Unit): Json ``` Two Kotlin features make the curly-brace syntax possible. ### 1. Trailing-lambda syntax Kotlin lets you move a lambda **out of the parentheses** when it is the **last** parameter of a call. So `Json({ ... })` can be written `Json { ... }`. If the lambda is the only argument, the parentheses disappear entirely. ### 2. Function type with receiver The parameter type `JsonBuilder.() -> Unit` is a **function type with receiver**. The `JsonBuilder.` prefix means: inside the lambda, **`this` is a JsonBuilder** — exactly as if the lambda body were a method on JsonBuilder. That is why you can write `isLenient = true` directly; it desugars to `this.isLenient = true`, and `this` resolves to the builder. ### What the function does internally The builder pattern here is: create a mutable builder, run the caller's lambda against it to mutate fields, then freeze it into an immutable result. ```kotlin // conceptual implementation class JsonBuilder { var isLenient: Boolean = false var ignoreUnknownKeys: Boolean = false // ... } fun Json(builderAction: JsonBuilder.() -> Unit): Json { val builder = JsonBuilder() // 1. mutable builder builder.builderAction() // 2. apply caller's lambda (this == builder) return JsonImpl(builder) // 3. produce immutable config } ``` The standard-library function `apply` is the same idea: `apply { ... }` is `T.() -> Unit` returning the receiver. ### Why a separate builder + immutable result The **builder** is mutable and only lives during configuration. The **result** (`Json`) is immutable and thread-safe at runtime. Separating them means callers cannot accidentally mutate config after construction. ### Key APIs/keywords - Function type with receiver: `Builder.() -> Unit` - Trailing-lambda call syntax - `this` rebinding inside the receiver lambda - `apply` / `run` as standard-library receiver-lambda helpers
- Why is the configured Json instance immutable while the builder is mutable?The builder exists only during setup; the resulting Json is read at runtime concurrently, so making it immutable avoids race conditions and accidental reconfiguration.
- What happens if the function's lambda parameter is NOT the last parameter?You cannot use trailing-lambda syntax; you must pass it inside the parentheses. DSL builders deliberately put the receiver lambda last.
Like handing someone a blank form (the builder) and saying 'fill in the fields'; when they hand it back you laminate it into a permanent card (the immutable config).
saying these in an interview costs you the question
- Thinking { } is a code block / anonymous object literal rather than a lambda argument
- Saying `this` inside the block refers to the enclosing class, not the builder
- Believing isLenient is a global variable rather than a builder property
- Confusing the mutable builder with the immutable result type