Patrick
Patrick

Reputation: 1291

Pattern for unvalidated data, validated data and validation

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

Answers (2)

marstran
marstran

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

IlyaMuravjov
IlyaMuravjov

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

Related Questions