If I am modeling my value objects using Kotlin data classes what is the best way to handle validation. Seems like the init block is the only logical place since it executes after the primary constructor.
data class EmailAddress(val address: String) { init { if (address.isEmpty() || !address.matches(Regex("^[a-zA-Z0-9]+@[a-zA-Z0-9]+(.[a-zA-Z]{2,})$"))) { throw IllegalArgumentException("${address} is not a valid email address") } } }
Using JSR-303 Example
The downside to this is it requires load time weaving
@Configurable data class EmailAddress(@Email val address: String) { @Autowired lateinit var validator: Validator init { validator.validate(this) } }
Android Dependency Injection using Dagger with KotlinThe primary constructor needs to have at least one parameter. All primary constructor parameters need to be marked as val or var. Data classes cannot be abstract, open, sealed, or inner. The class may extend other classes or implement interfaces.
Creating a custom validation constraint The extension method is implemented by calling validate(..) with our Constraint and passing a validation function. In the validation function we implement the actual validation. In this example we simply convert the Iterable to a List and then the List to a Set.
Data classes specialize in holding data. The Kotlin compiler automatically generates the following functionality for them: A correct, complete, and readable toString() method. Value equality-based equals() and hashCode() methods.
Inheriting a data class from another data class is not allowed because there is no way to make compiler-generated data class methods work consistently and intuitively in case of inheritance.
I did make a comment, but I thought I would share my approach to validation instead.
First, I think it is a mistake to perform validation on instantiation. This will make the boundary between deserialization and handing over to your controllers messy. Also, to me, if you are sticking to a clean architecture, validation is part of your core logic, and you should ensure with tests on your core logic that it is happening.
So, to let me tackle this how I wish, I first define my own core validation api. Pure kotlin. No frameworks or libraries. Keep it clean.
interface Validatable { /** * @throws [ValidationErrorException] */ fun validate() } class ValidationErrorException( val errors: List<ValidationError> ) : Exception() { /*** * Convenience method for getting a data object from the Exception. */ fun toValidationErrors() = ValidationErrors(errors) } /** * Data object to represent the data of an Exception. Convenient for serialization. */ data class ValidationErrors( val errors : List<ValidationError> ) data class ValidationError( val path: String, val message: String )
Then I have a framework specific implementations. For example a javax.validation.Validation
implementation:
open class ValidatableJavax : Validatable { companion object { val validator = Validation.buildDefaultValidatorFactory().validator!! } override fun validate() { val violations = validator.validate(this) val errors = violations.map { ValidationError(it.propertyPath.toString(), it.message) }.toMutableList() if (errors.isNotEmpty()) { throw ValidationErrorException(errors = errors) } } }
The only problem with this, is that the javax annotations don't play so well with kotlin data objects - but here is an example of a class with validation:
import javax.validation.constraints.Positive class MyObject( myNumber: BigDecimal ) : ValidatableJavax() { @get:Positive(message = "Must be positive") val myNumber: BigDecimal = myNumber }
It seems unreasonable to me to have object creation validation anywhere else but in the class constructor. This is the place responsible for the creation, so that is the place where the rules which define what is and isn't a valid instance should be. From a maintenance perspective it also makes sense to me as it would be the place where I would look for such rules if I had to guess.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With