Kotlin DSLs are often used for configuration or UI building, but they rarely enforce correctness at compile or runtime. You can enhance DSLs with declarative validation logic, using internal builder patterns to collect multiple errors at once—an uncommon but highly powerful pattern.
✅ Use Case: Validating a DSL for Form Submission Logic
Instead of failing fast on the first error, we want to collect all configuration errors and then fail with full diagnostics.
⚙️ DSL with Validation Support
@DslMarker
annotation class FormDsl
@FormDsl
class FormBuilder {
private val errors = mutableListOf<String>()
private var name: String? = null
private var email: String? = null
fun name(value: String) {
if (value.isBlank()) errors += "Name cannot be blank"
name = value
}
fun email(value: String) {
if (!value.contains("@")) errors += "Invalid email format"
email = value
}
fun build(): Form {
if (errors.isNotEmpty()) {
throw IllegalArgumentException("Form validation failed:\n" + errors.joinToString("\n"))
}
return Form(name!!, email!!)
}
}
data class Form(val name: String, val email: String)
fun form(block: FormBuilder.() -> Unit): Form =
FormBuilder().apply(block).build()
✅ Usage:
val form = form {
name("")
email("no-at-symbol")
}
// Throws with detailed error report:
// - Name cannot be blank
// - Invalid email format
📌 Notes for Quick Reference
• ✅ Use errors: MutableList to accumulate configuration mistakes.
• ✅ Use @DslMarker to prevent scope collisions.
• ❌ Avoid throwing inside individual DSL methods—defer until .build().
• ✅ Ideal for CI/CD DSLs, Terraform-style Kotlin config, or complex form builders.
🚀 Pro Tip: Add warning() support next to error() to flag non-blocking suggestions during DSL execution, mimicking compiler diagnostics.
Top comments (0)