Reputation: 1291
In my Ktor REST API, I receive JSON. This is conveniently deserialised for me into simple data classes by kotlinx.serialization
. Before I can proceed using this data I have to apply some validation. Then I can pass it on to to other parts of the code.
I would like a clear separation between deserialised but unvalidated data, the validation itself, and validated code.
For example, I have something to the effect of:
@Serializable
data class Input(val something: Int)
fun Input.validate() {
if(something < 5) { throw WhateverException("invalid") }
}
But there's a few issues with this: When receiving an instance of Input
anywhere, I can't be sure it's been validated already, so to be safe I'll call validate()
again.
So, to avoid this I would like for validate()
to return some version of Input
that tells me the data is valid, and so that I can have method signatures in my codebase that accept validated data only.
I know I can copy the class Input
to a private one called ValidatedInput
and have validate()
return that, but that looks like code duplication. (I will end up having dozens of classes like Input
.) Also that will prevent be from having an interface specifying Input
to have the validate
method and have it return something like Validated<Input>
How do I design my classes and methods to clearly express this separation, and without repeating code?
Upvotes: 0
Views: 464
Reputation: 28056
If you want to make sure that Input
is validated, you have to somehow put it in the type system. Since Validated<Input>
is a no-go for you (I would prefer doing it this way), maybe you could turn it around by giving Input
some kind of validation token as a type parameter?
For example like this:
data class Input<out Validation>(val something: Int)
sealed class Validation
object Validated : Validation()
object NotValidated : Validation()
fun Input<NotValidated>.validate(): Input<Validated> {
return if(something < 5) { throw RuntimeException("invalid") }
else Input<Validated>(something)
}
Doing it the other way (Validated<Input>
) is a bit more flexible in my opinion. It also doesn't mix any code about validation into the Input
class (like the validation token would). So, for example, you could do something like this:
sealed class Validated<T>
class Valid<T>(val value: T) : Validated<T>()
class Invalid<T>(val error: Exception): Validated<T>()
fun Input.validate(): Validated<Input> {
return if(something < 5) Invalid(RuntimeException("invalid"))
else Valid(this)
}
fun performAction(input: Valid<Input>) {
TODO("Do something with the valid Input")
}
fun main() {
val validated = Input(5).validate()
val result = when(validated) {
is Valid -> performAction(validated)
is Invalid -> throw validated.error
}
println(result)
}
Upvotes: 1
Reputation: 2502
You can move validation to Input
initialization:
@Serializable
data class Input(val something: Int) {
init {
validate()
}
}
If you do it all Input
instances will be valid.
Upvotes: 0