Without @DslMarker, what bug can appear in a nested markup DSL, and how does annotating tag types with a @DslMarker annotation fix it?
answer
- Problem: outer tag functions leak into inner blocks
- @DslMarker is a meta-annotation on your own annotation
- Annotate the common tag base class
- Same-marker receivers: only innermost implicit
- Outer still reachable via this@Label
basics
~20 sWithout it, an inner block can accidentally call functions from an outer tag, building a wrong tree. @DslMarker tells the compiler to hide outer receivers inside nested blocks, so only the closest tag's functions are available implicitly.
solid answer
~40 sIn nested receiver lambdas, every enclosing receiver stays an implicit receiver. So inside head { } you could call body { } (a function of the outer HTML), silently attaching body in the wrong place — a real correctness bug. @DslMarker fixes this: you create an annotation class annotated @DslMarker, then annotate your tag types (or their common base) with it. The compiler treats two implicit receivers that share the same DSL-marker annotation as conflicting; only the nearest one is accessible implicitly. The outer receiver isn't gone — you can still reach it explicitly via this@HTML — but unqualified calls to outer-tag functions now fail to compile, catching the structural mistake. kotlinx.html applies its @HtmlTagMarker to all tags for exactly this reason.
code
kotlin · 22 lines@DslMarker
annotation class HtmlTagMarker
@HtmlTagMarker
abstract class Tag(val name: String) {
val children = mutableListOf<Any>()
}
class HTML : Tag("html") {
fun head(b: HEAD.() -> Unit) { children += HEAD().apply(b) }
fun body(b: BODY.() -> Unit) { children += BODY().apply(b) }
}
class HEAD : Tag("head")
class BODY : Tag("body")
fun html(b: HTML.() -> Unit) = HTML().apply(b)
val ok = html {
head { }
body {
// head { } // compile error thanks to @DslMarker
this@html.head { } // allowed: explicit outer receiver
}
}go deeper
Can state that @DslMarker stops inner blocks from accidentally using outer tag functions.
Knows you define a meta-annotated annotation, put it on tag types, and that only the nearest same-marked receiver stays implicit.
Explains the leakage bug concretely, the same-marker conflict rule, and that this@Label is the explicit escape hatch.
Frames it as turning structural discipline into compile-time guarantees, discusses marker placement on a base class and cross-DSL interactions of distinct markers.
## The leakage problem Receiver lambdas stack their receivers. Inside deeply nested DSL blocks, **all** enclosing tags remain implicit receivers, so their builder functions are callable without a qualifier: ```kotlin html { // this: HTML head { // this: HEAD, but HTML still in scope head { } // OOPS: HTML.head() is reachable here -> nested head inside head } } ``` This compiles and produces a malformed tree. The bug is silent because the outer receiver's functions are perfectly valid candidates for unqualified resolution. ## @DslMarker to the rescue `@DslMarker` is a meta-annotation. You define your own marker and apply it to the tag types: ```kotlin @DslMarker annotation class HtmlTagMarker @HtmlTagMarker abstract class Tag(val name: String) { /* ... */ } class HTML : Tag("html") { fun head(b: HEAD.() -> Unit) { /* ... */ } } class HEAD : Tag("head") { /* ... */ } ``` Now the rule kicks in: **if two implicit receivers in scope are marked with the same DSL-marker annotation, only the innermost one is available as an implicit receiver.** The outer `HTML` receiver becomes inaccessible for *unqualified* calls inside `head { }`, so `head { }` (an `HTML` member) no longer resolves implicitly — the mistaken nesting fails to compile. ## You can still reach the outer receiver explicitly `@DslMarker` only restricts **implicit** access. When you genuinely need the parent, use a **labeled this**: ```kotlin html { body { [email protected] { } // explicit, compiles } } ``` ## Key points to remember - The annotation must itself be annotated `@DslMarker` (a meta-annotation), typically with `@Target(AnnotationTarget.CLASS)`. - Apply it once on a **common base class** so all tags inherit the marker. - It compares markers by **annotation type**: receivers marked with the *same* marker conflict; receivers with *different* markers don't. - It changes **resolution**, not the object graph — `this@Outer` still works. - kotlinx.html uses `@HtmlTagMarker`; Ktor, kotlinx.html, and Gradle Kotlin DSL all use this technique. ## Why it matters at senior level Without `@DslMarker`, a markup or config DSL is only safe by discipline. With it, the type system enforces that each block can only configure its own node, turning a class of silent structural bugs into compile errors — the whole point of a *type-safe* builder.
- Does @DslMarker remove the outer receiver entirely?No. It only blocks implicit (unqualified) access. The outer receiver is still reachable with a labeled this, e.g. this@html.
- What happens if two nested receivers carry different DSL-marker annotations?They don't conflict, so both remain implicitly accessible — the restriction applies only between receivers sharing the same marker annotation type.
- Where should you place the marker annotation in a tag hierarchy?On a common base class, so every tag inherits it and the whole DSL is covered consistently.
It's like a privacy screen: inside the inner room you can only touch the inner room's controls; the outer room's controls are hidden unless you point at them by name.
saying these in an interview costs you the question
- Thinking @DslMarker is applied directly to functions or lambdas
- Believing it deletes/hides the parent object rather than restricting implicit resolution
- Not knowing this@Label still reaches the outer receiver
- Claiming different markers also conflict
- Saying it's a runtime check rather than compile-time