When building a nested config/routing DSL, why might inner blocks accidentally call outer-scope builder functions, and how does @DslMarker fix it?
answer
- Nested DSLs stack multiple implicit receivers
- Outer builder callable by accident -> wrong tree
- @DslMarker on your annotation, then on receiver classes
- Same-marker outer receiver: implicit access -> compile error
- Escape with this@outer (labeled this)
basics
~20 sIn nested blocks, both the inner and outer this receivers are in scope, so you can call an outer builder by accident. @DslMarker on the receiver types blocks the implicit outer receiver, forcing you to be explicit.
solid answer
~40 sIn a nested receiver-lambda DSL (e.g. `routing { route("/a") { route("/b") { } } }`), multiple implicit receivers are simultaneously in scope. Without protection, inside the innermost block you could call a function meant for an outer receiver — e.g. nest two `get` calls in a way that resolves to the wrong node — silently producing a wrong tree. `@DslMarker` is a meta-annotation: you annotate your own annotation with it, then apply that annotation to the DSL receiver classes. The compiler then enforces that within a scope **only the nearest receiver of each DSL-marker group is accessible implicitly**. Calling an outer same-marker receiver's member implicitly becomes a compile error; you must qualify it (`[email protected]()`). kotlinx.html uses `@HtmlTagMarker`; Ktor and Gradle Kotlin DSL use the same mechanism. It makes nested builder DSLs safe by construction.
code
kotlin · 20 lines@DslMarker
annotation class TreeDsl
@TreeDsl class Node(val name: String) {
val children = mutableListOf<Node>()
fun node(name: String, build: Node.() -> Unit) {
children += Node(name).apply(build)
}
}
fun tree(build: Node.() -> Unit) = Node("root").apply(build)
val t = tree {
node("a") {
node("b") {
// node("x") here attaches to b, never to a/root by accident
this@tree.node("explicit-on-root") {} // must qualify to reach outer
}
}
}go deeper
May not know the hazard; can recognize @DslMarker is about DSL scope safety.
Explains the implicit-receiver accidental-call problem and that @DslMarker restricts outer access.
Correctly describes the meta-annotation mechanism, same-marker rule, and labeled-this escape.
Designs marker hierarchies for a multi-layer DSL and weighs ergonomics vs strictness, citing kotlinx.html/Ktor practice.
## The implicit-receiver hazard Receiver-lambda DSLs nest by stacking implicit receivers. Inside a deeply nested block, **all** enclosing receivers are still implicitly available. Kotlin's overload resolution will happily bind a call to an outer receiver if the inner one has no matching member. That can silently build the wrong structure. ```kotlin class Html { fun body(b: Body.() -> Unit) {} } class Body { fun p(b: P.() -> Unit) {} } class P fun html(b: Html.() -> Unit) {} html { body { // Without @DslMarker, `body { }` here would compile, // resolving to the OUTER Html receiver -> nonsense tree. body { } } } ``` ## How @DslMarker fixes it `@DslMarker` is a **meta-annotation** applied to your own annotation class: ```kotlin @DslMarker annotation class MyDsl @MyDsl class Html { fun body(b: Body.() -> Unit) {} } @MyDsl class Body { fun p(b: P.() -> Unit) {} } ``` Rule the compiler enforces: **within a lambda, for receivers that carry the same `@DslMarker` annotation, only the innermost one is accessible implicitly.** A member of an outer same-marker receiver can no longer be called implicitly — that becomes a **compile error**. The earlier mistaken nested `body { }` now fails to compile. ## Escaping when you really mean the outer receiver When you genuinely need the outer receiver, qualify it with a **labeled this**: ```kotlin html { // this: Html, label this@html body { // this: Body [email protected] { } // explicit -> allowed } } ``` ## Where this shows up in real DSLs - **kotlinx.html** marks tag receivers with `@HtmlTagMarker`. - **Ktor** marks routing/config receivers so you cannot accidentally call an outer-node builder from an inner block. - **Gradle Kotlin DSL** uses markers (e.g. on script/extension types) to keep block scopes from leaking. ## Important nuance `@DslMarker` only restricts **implicit** access to receivers sharing the **same** marker annotation. Receivers with *different* markers, or extension functions, are unaffected. The restriction is purely a compile-time scoping rule — it does not change runtime behavior. ### Key APIs/keywords - `@DslMarker` meta-annotation - Custom annotation class applied to receiver types - Labeled this: `this@outer` - Implicit vs explicit receiver resolution - Real markers: `@HtmlTagMarker`
- Does @DslMarker restrict receivers that carry DIFFERENT marker annotations?No. The restriction only applies between receivers sharing the same @DslMarker-annotated annotation; different markers and extension receivers are unaffected.
- How do you intentionally call an outer receiver's builder once @DslMarker is applied?Use a labeled/qualified this, e.g. [email protected](), making the outer receiver explicit.
Like nested meeting rooms: by default you can shout into the outer room from the inner one. @DslMarker soundproofs same-type rooms so you must explicitly address the outer room by name.
saying these in an interview costs you the question
- Thinking @DslMarker changes runtime behavior or performance
- Applying @DslMarker directly to the builder functions instead of via a custom annotation on receiver types
- Claiming it blocks ALL outer receivers, including different markers
- Not knowing the labeled-this escape hatch
- Believing it is required for a DSL to compile at all