Validations in Spring with Kotlin
@UniqueName
data class CreateRequest(
val id: String = "",
@get:NotBlank(message = "{req.name.blank}")
val name: String = "",
@get:NotBlank(message = "{req.description.blank}")
val description: String = "",
@get:Valid
val nested: NestedClass = NestedClass("", ""),
)
data class NestedClass(
@get:NotBlank(message = "{req.nested.something.blank}")
val something: String,
@get:NotBlank(message = "{req.nested.something_else.blank}")
val somethingElse: String
)
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@Constraint(validatedBy = [UniqueNameValidator::class])
annotation class UniqueName(
val message: String = "",
val groups: Array<KClass<*>> = [],
val payload: Array<KClass<out Payload>> = []
)
class UniqueNameValidator(
private val sampleService: SampleService
) : ConstraintValidator<UniqueName, CreateRequest> {
override fun isValid(request: CreateRequest, context: ConstraintValidatorContext): Boolean {
val dupSample = sampleService.findByName(request.name)
val result = !(dupSample != null && dupSample.id != project.id)
if (!result) {
context.disableDefaultConstraintViolation()
context
.buildConstraintViolationWithTemplate("{req.name.unique}")
.addPropertyNode("name")
.addConstraintViolation()
}
return result
}
}
src/main/resources/ValidationMessages.properties
req.name.blank=Name is required.
req.name.unique=Name must be unique.
req.description.blank=Description is required.
req.nested.something.blank=Something is required.
req.nested.something_else.blank=Something Else is required.
Custom Response Messages
@ControllerAdvice
class ControllerAdvice {
@ExceptionHandler(value = [MethodArgumentNotValidException::class])
fun handleMethodArgumentNotValid(exception: MethodArgumentNotValidException): ResponseEntity<ValidationError> {
val violations = exception.bindingResult.allErrors
.mapNotNull { error ->
when (error) {
is FieldError -> Violation(error.field.toSnakeCase(), error.defaultMessage ?: "")
is ObjectError -> Violation(error.objectName.toSnakeCase(), error.defaultMessage ?: "")
else -> null
}
}
.toList()
return ResponseEntity.status(BAD_REQUEST).body(ValidationError(errors = violations))
}
}
class ValidationError(
val status: String = BAD_REQUEST.toString(),
val message: String = "Errors occurred during validation",
val errors: List<Violation>
)
class Violation(val field: String, val message: String)
fun String.toSnakeCase(): String = Normalizer
.normalize(this, Normalizer.Form.NFD)
.replace("([A-Z]+)".toRegex(), "\\_\$1")
.toLowerCase()
Controller
@PostMapping
fun create(@Valid @RequestBody body: CreateRequest): SampleDocument {
return sampleService.save(body)
}
Sample Response
{
"status": "400 BAD_REQUEST",
"message": "Errors occurred during validation",
"errors": [
{
"field": "nested.something_else",
"message": "Something Else is required."
},
{
"field": "name",
"message": "Name must be unique."
},
{
"field": "nested.something",
"message": "Something is required."
},
{
"field": "name",
"message": "Name is required."
},
{
"field": "description",
"message": "Description is required."
}
]
}