What problem does the @DslMarker annotation solve in a Kotlin type-safe builder DSL?
answer
- Meta-annotation on your own annotation
- Hides OUTER implicit receiver in nested block
- Same marker = only nearest receiver visible
- Escape with [email protected]()
- kotlinx.html uses @HtmlTagMarker
basics
~10 sIn a nested builder block, methods from the outer block are still callable by accident. @DslMarker hides the outer receiver inside inner blocks, so you only see the members that belong where you are.
solid answer
~40 sType-safe builder DSLs use lambdas with receivers (A.()->Unit), so inside a block 'this' is the builder. When you nest blocks, both the inner and outer receivers are implicitly in scope, so an outer method like html { body { title("x") } } could accidentally be called inside body even though it belongs to html. @DslMarker is a meta-annotation: you create your own annotation (e.g. @HtmlDsl) annotated with @DslMarker and put it on the receiver types/classes. Then the compiler enforces that within a nested block only the nearest receiver of that marker is accessible implicitly; outer same-marker receivers are hidden. Calling the outer member then becomes a compile error unless you qualify it explicitly (e.g. [email protected](...)). It makes the DSL safer and the autocomplete cleaner.
code
kotlin · 16 lines@DslMarker
annotation class HtmlDsl
@HtmlDsl
class HtmlBuilder { fun head(block: HeadBuilder.() -> Unit) {} ; fun body(block: BodyBuilder.() -> Unit) {} }
@HtmlDsl class HeadBuilder
@HtmlDsl class BodyBuilder
fun html(block: HtmlBuilder.() -> Unit) = HtmlBuilder().apply(block)
fun page() = html {
body {
// head { } // ERROR: outer HtmlBuilder receiver is hidden
this@html.head { } // OK: explicitly qualified
}
}go deeper
Can state that it prevents accidentally calling outer-block methods inside a nested builder block.
Explains the implicit-receiver stacking problem and that you create a custom annotation marked with @DslMarker and apply it to receiver classes.
Knows the exact rule (same marker, only nearest receiver visible), the this@label escape hatch, and that it can annotate the function type too.
Can reason about DSL API design trade-offs, when multiple distinct markers are appropriate, and how this affects discoverability and misuse-resistance of a published DSL.
## The setup: lambdas with receivers Kotlin DSLs are built from **function types with a receiver**, written `A.() -> Unit`. Inside such a lambda, `this` refers to an instance of `A` (the *implicit receiver*), so you can call `A`'s members without qualification. A builder is just nested lambdas-with-receivers: ```kotlin html { // this: HtmlBuilder head { /* this: HeadBuilder */ } body { // this: BodyBuilder // ... } } ``` ## The problem: implicit receivers stack Inside the `body { }` block, **two** implicit receivers are in scope: `BodyBuilder` (nearest) and `HtmlBuilder` (the enclosing one). Both are available unqualified. So if `HtmlBuilder` has a member like `head { }` or `meta()`, you can accidentally call it *inside* `body`, where it makes no semantic sense: ```kotlin body { head { } // compiles! but logically wrong — head belongs to html } ``` This is a silent correctness bug and pollutes autocomplete. ## The fix: @DslMarker `@DslMarker` is a **meta-annotation** (an annotation you put on *your own* annotation). You define a marker once: ```kotlin @DslMarker annotation class HtmlDsl ``` Then you apply that marker to every receiver type in the DSL (`@HtmlDsl class HtmlBuilder`, `@HtmlDsl class BodyBuilder`, …). The rule the compiler then enforces: > Within the scope of two implicit receivers that carry the **same** `@DslMarker` annotation, only the **nearest** one is accessible without an explicit qualifier. The outer one is hidden. So `head { }` inside `body { }` now becomes a **compile error**, because `HtmlBuilder` (outer, same marker) is shadowed by `BodyBuilder`. ## Escaping the restriction The outer receiver is hidden, not gone. You can still reach it with a **qualified `this`** using a label: ```kotlin html { // this@html: HtmlBuilder body { [email protected] { } // explicit — compiles } } ``` ## Key facts - `@DslMarker` itself has `@Target(ANNOTATION_CLASS)` and `@Retention(BINARY)`. - The restriction only applies between receivers carrying the **same** marker annotation. Different markers don't shadow each other. - It can be applied to the class, or to the **function type** of the parameter (e.g. `@HtmlDsl HtmlBuilder.() -> Unit`). - The canonical real example is `@DslMarker annotation class HtmlTagMarker` in `kotlinx.html`. ## Why it matters Without it, deeply nested builders leak every ancestor's API into every block. With it, each block exposes exactly the members that belong there — safer DSLs and cleaner IDE suggestions.
- Does @DslMarker restrict receivers that carry different marker annotations?No. The shadowing rule only applies between two implicit receivers annotated with the same @DslMarker annotation. Receivers with different markers (or none) are unaffected.
- Is the outer member call permanently forbidden inside the nested block?No, only the implicit (unqualified) call is forbidden. You can still call it explicitly via a qualified this, e.g. [email protected] { }.
Like nested rooms where the inner room's walls block you from grabbing tools off the outer room's shelf unless you deliberately reach back through the door.
saying these in an interview costs you the question
- Saying @DslMarker is applied directly to functions or properties rather than being a meta-annotation on an annotation class
- Claiming it works across different marker annotations
- Thinking it removes the outer receiver entirely (it's just hidden from implicit resolution)
- Confusing it with @JvmStatic or @JvmName
- Believing you must write @DslMarker on every call site