| name | serialization-validation |
| description | Load when creating DTOs, configuring JSON serialization, or adding input validation. Covers @SerialName conventions, JSON config, and validation patterns. |
When to use me
- Creating or modifying DTO classes with kotlinx.serialization
- Configuring JSON content negotiation
- Adding input validation for API requests
- Mapping JSON field names
Not intended for
- Database models (DBO) → use
data-access
- General data layer → use
data-access
Kotlinx Serialization Conventions
DTO Naming
- All DTOs use
@SerialName on every field (mandatory):
@Serializable
data class UserDTO(
@SerialName("id") val id: String? = null,
@SerialName("username") val username: String,
@SerialName("email") val email: String,
@SerialName("image") val image: String,
@SerialName("lang") val lang: String = "en",
)
This ensures consistent JSON field naming regardless of Kotlin property names.
@Serializable
All DTOs must be annotated with @Serializable:
import kotlinx.serialization.Serializable
@Serializable
data class AcknowledgeDTO(
@SerialName("acknowledge") val acknowledge: Boolean
)
JSON Config
Currently configured with defaults in plugins/Serialization.kt:
fun Application.configureSerialization() {
install(ContentNegotiation) {
json()
}
}
For custom JSON behavior (e.g., pretty print, ignore unknown keys):
json(Json {
prettyPrint = true
ignoreUnknownKeys = true
isLenient = true
})
Request Body Deserialization
Ktor automatically deserializes request bodies to DTOs:
post {
val request = call.receive<SoundRequestDTO>()
}
Validation Patterns
Manual validation with extension functions
fun String.isEmail(): Boolean =
Pattern.compile("^[\\w-.]+@([\\w-]+\\.)+[\\w-]{2,4}\$").matcher(this).matches()
if (!email.isEmail()) {
call.respond(HttpStatusCode.BadRequest, "Invalid email")
return@post
}
Guard clause pattern in routes
post("/register") {
val registerDTO = call.receive<RegisterDTO>()
if (registerDTO.username.isBlank()) {
return@post call.respond(HttpStatusCode.BadRequest, "Username is required")
}
if (!registerDTO.email.isEmail()) {
return@post call.respond(HttpStatusCode.BadRequest, "Invalid email")
}
}
Required parameter validation
suspend fun RoutingContext.requireString(fieldName: String, block: suspend RoutingContext.(String) -> Unit) {
call.parameters[fieldName]?.takeIf { it.isNotEmpty() }?.let {
block(it)
} ?: call.respond(HttpStatusCode.BadRequest, "$fieldName is required")
}
get {
requireString("id") { id ->
}
}
Required integer parameter validation
suspend fun RoutingContext.requireInt(fieldName: String, block: suspend RoutingContext.(Int) -> Unit) {
call.parameters[fieldName]?.takeIf { it.isNotEmpty() }?.toIntOrNull()?.let {
block(it)
} ?: call.respond(HttpStatusCode.BadRequest, "$fieldName is required")
}
Validation Checklist for New Endpoints
Blockers (MUST NOT)
- Missing
@Serializable on DTOs (will cause runtime serialization errors)
- Using
@SerialName inconsistently (some fields with, some without)
- Returning raw DBO/BO objects from routes — always map to DTO
- Trusting user input without validation — validate before processing
- Using field names as JSON keys without explicit
@SerialName
References
plugins/Serialization.kt — JSON content negotiation setup
data/dto/ — all existing DTOs with @SerialName patterns
utils/PatternUtils.kt — email validation
utils/RouteExtensions.kt — parameter validation utilities
routing/AuthRouting.kt — real-world validation examples