1/23/2024, 9:27:10 PM
Spring Boot’s Bean Validation system comes with a lot of built-in annotations that can be applied to fields
like NotBlank
, Email
, etc.
What it does not include though is a validation for a very common use case for registration and password reset forms which uses the confirm password field to prevent accidental typos while setting the password.
However, thanks to the dynamic annotation based nature of Spring Boot’s validation system, it’s very easy to create a custom annotation that handles this use case for us.
The implementation is made up of three parts that come together to perform the validation. The validation itself is designed to be as generic as possible so that it can be reused for multiple data classes.
An interface that defines the common password fields, which allows the validation logic to be type-safe.
interface ConfirmPasswordInterface {
val password: String
val confirmPassword: String
}
The annotation itself with its properties. Of particular note is the @Target
attribute, which specifies this as a
class level annotation instead of the usual field level annotations.
The field message
defines the error message sent back in the response that can be overridden to pass a custom error
message if needed.
The @Constraint
annotation defines the actual validator implementation. The ConfirmPasswordValidator
class will be
used for the checks.
@Target(AnnotationTarget.CLASS)
@Constraint(validatedBy = [ConfirmPasswordValidator::class])
@Retention(AnnotationRetention.RUNTIME)
annotation class ConfirmPassword(
val message: String = "The passwords do not match",
val groups: Array<KClass<Any>> = [],
val payload: Array<KClass<Payload>> = [],
)
The validator class that contains the validation logic and performs the actual validation on the fields by inheriting
the ConstraintValidator
interface from Spring, which implements an isValid
function that Spring calls to perform the
validation.
The form data class marked with our annotation is passed as a parameter to the isValid
. Since the data class
implements our interface, we can directly access the properties here without having to resort to reflection.
Since the annotation is applied to the class instead of a field, Spring is unable to attach the error message to the
correct field. To resolve this, we can use the context
parameter to attach the error message to the correct field
ourselves.
class ConfirmPasswordValidator : ConstraintValidator<PasswordConfirmation, ConfirmPasswordInterface> {
override fun isValid(request: ConfirmPasswordInterface, context: ConstraintValidatorContext): Boolean {
val isValid = request.password == request.passwordConfirmation
if (!isValid) {
context.disableDefaultConstraintViolation()
context
.buildConstraintViolationWithTemplate(context.defaultConstraintMessageTemplate)
.addPropertyNode("passwordConfirmation")
.addConstraintViolation()
}
return isValid
}
}
That’s it. While I prefer to keep all the three parts in the same file, you can keep then in separate files if that is how your codebase or style guidelines prefer.
To implement this validation on data classes, all you have to do is implement a data class with the
ConfirmPasswordInterface
and then attach the ConfirmPassword
annotation to the class.
Below is an example of a simple registration form validator. In this example, the data class also overrides the password fields to add more validations to the fields themselves.
@ConfirmPassword
data class RegisterRequest(
@field:NotBlank(message = "This field is required")
@field:Size(max = 255, message = "This field should be smaller that 255 characters")
val firstName: String,
@field:NotBlank(message = "This field is required")
@field:Size(max = 255, message = "This field should be smaller that 255 characters")
val lastName: String,
@field:NotBlank(message = "This field is required")
@field:Email(message = "The email address is invalid")
@field:Size(max = 255, message = "This field should be smaller that 255 characters")
val email: String,
@field:NotBlank(message = "This field is required")
override val password: String,
@field:NotBlank(message = "This field is required")
override val confirmPassword: String,
) : ConfirmPasswordInterface