skip to content

Show how to define and apply a @DslMarker annotation. Where exactly must the marker be placed for the scope restriction to take effect?

level: middleimportance: must knowfreq 45%

answer

  1. @DslMarker target = ANNOTATION_CLASS only
  2. Put the custom annotation on receiver CLASSES
  3. Or on the function-type receiver: @M Builder.()->Unit
  4. Must reach the receiver TYPE to matter
  5. One shared marker for the whole DSL

basics

~10 s

Create an annotation class and mark it with @DslMarker. Then put that annotation on the builder/receiver classes (or on the lambda's receiver type). The restriction kicks in when two implicit receivers share that marker.

solid answer

~40 s

Define `@DslMarker annotation class MyDsl`. @DslMarker is a meta-annotation, so MyDsl now propagates the DSL-scope behavior. Apply MyDsl to each receiver type that participates in the DSL — typically the builder classes (`@MyDsl class TableBuilder`). The compiler then treats any two implicit receivers both annotated with MyDsl as conflicting: inside the inner one, the outer is hidden. You can also annotate the function-type receiver directly: `fun row(init: @MyDsl RowBuilder.() -> Unit)`, useful when you don't own/can't annotate the receiver class. The annotation propagates through type usages, so annotating the class is the most common and robust approach. The marker must reach the receiver TYPE; putting it elsewhere (a function, a property) does nothing for scope control.

code

kotlin · 14 lines
kotlin
@DslMarker
annotation class TableDsl

@TableDsl class TableBuilder { fun row(init: RowBuilder.() -> Unit) {} }
@TableDsl class RowBuilder { fun cell(text: String) {} }

fun table(init: TableBuilder.() -> Unit) = TableBuilder().apply(init)

fun demo() = table {
    row {
        cell("a")    // OK
        // row { }   // ERROR: outer TableBuilder is shadowed
    }
}

go deeper

for a junior

Recognizes that you define an annotation and mark it @DslMarker, but may be fuzzy on exactly where to apply it.

for a middle

Correctly applies the custom annotation to receiver classes and knows the effect on nested calls.

for a senior

Knows the alternative of annotating the function-type receiver for types you don't own, and that @DslMarker's target is ANNOTATION_CLASS.

for a principal

Designs the marker strategy for a whole library, deciding single vs multiple markers and ensuring every receiver type is covered consistently.

## Step 1 — define the marker `@DslMarker` is a built-in **meta-annotation** in `kotlin` package. You apply it to *your own* annotation class: ```kotlin @DslMarker annotation class TableDsl ``` That's the whole definition. `TableDsl` now carries DSL-scope semantics. (`@DslMarker` is declared with `@Target(AnnotationTarget.ANNOTATION_CLASS)`, so it can *only* be placed on an annotation class — the compiler rejects it elsewhere.) ## Step 2 — apply it to the receiver types The scope restriction is keyed on the **type of the implicit receiver**. So the marker must end up on those types. Two ways: **A) Annotate the builder class (most common):** ```kotlin @TableDsl class TableBuilder { fun row(init: RowBuilder.() -> Unit) {} } @TableDsl class RowBuilder { fun cell(text: String) {} } ``` **B) Annotate the receiver of the function type** (when you can't modify the class): ```kotlin fun table(init: @TableDsl TableBuilder.() -> Unit) = TableBuilder().apply(init) ``` The `@TableDsl` before `TableBuilder.()` annotates the *receiver type usage*. This is handy for builders over third-party types. ## What now becomes an error ```kotlin table { // this: TableBuilder row { // this: RowBuilder row { } // ERROR: outer TableBuilder hidden (same marker) cell("x") // OK: cell belongs to RowBuilder (nearest) } } ``` Before the marker, `row { }` inside `row { }` would have compiled. ## Where it must NOT go (no effect) - On a function/method: pointless for scope control. - On a property: pointless. - Forgetting to mark even one receiver class breaks the chain at that level — that receiver won't be shadowed. ## Multiple markers If two receiver types use **different** markers, no shadowing happens between them. So a single, shared marker for the whole DSL is usually what you want; use separate markers deliberately when you actually want cross-scope access (e.g. an HTML DSL nested inside an unrelated config DSL). ## Recap of the mechanism 1. `@DslMarker` on annotation class → makes it a DSL marker. 2. Marker on receiver types → activates same-marker shadowing. 3. Shadowing → only nearest implicit receiver of that marker is unqualified-accessible. 4. Qualified `this@label` always escapes.

  • Can you apply @DslMarker to a function?
    No. @DslMarker has @Target(ANNOTATION_CLASS), so the compiler only allows it on an annotation class. You apply the resulting custom annotation (not @DslMarker itself) to receiver types.
  • When would you annotate the function-type receiver instead of the class?
    When you don't own the receiver class (e.g. a builder over a third-party or stdlib type) and therefore can't put the marker on the class declaration.

saying these in an interview costs you the question

  • Putting @DslMarker (rather than the custom annotation) on the builder classes
  • Annotating functions/properties expecting scope control
  • Forgetting that the marker must reach the receiver type
  • Believing each nested level needs a different marker
  • Thinking applying it once anywhere covers the whole DSL automatically

context