konform-kt / konform

Portable validations for Kotlin
https://www.konform.io
MIT License
651 stars 39 forks source link

Advise on how to structure data classes to produce a strongly typed validated data #31

Closed andrewliles closed 2 years ago

andrewliles commented 2 years ago

I am trying to find a good way to validate data from Spring MVC implementing a REST POST endpoint. There are two obstacles with Kotlin... suppose we have this data class:

data class Message(
    val message: String,
    val level: System.Logger.Level //an enum
)

If either parameter is not supplied, Spring passes nulls and these cause a constructor violation before any validation can run, and secondly if the enum value is illegal again the data class does not get instantiated.

So I have to use a permissive data class like this:

data class Message(
    val message: String?,
    val level: String?
)

Konform can easily validate this using constraints like this:

    fun validate() = Validation<Message> {
        Message::message required {}
        Message::level required { enum<System.Logger.Level>() }
    }(this)

but still to use the data class, even though I know the data to be valid, I have to make calls like:

        request.message!!
        System.Logger.Level.valueOf(request.level!!)

it would be better to do something like the following which fully encapsulates all this permissive typing:

class Message(
    message: String?,
    level: String?
) {
    init {
        Validation<Message> {
            message required {}
            level required { enum<System.Logger.Level>() }
        }.validate(this)
    }
    val message = message!!
    val level = System.Logger.Level.valueOf(level!!)
}

and exposes the public properties of the non-optional String and non-optional enum value. However, this doesn't work, because the validator cannot bind to the constructor parameters.

Is there another way? - perhaps by using Validation<String> {.. on the fields one by one, but then a useful method like ValidationReseult.combineWith is not available to me.

nlochschmidt commented 2 years ago

konform is not a parser so data has to be parsed before being validated.

You could still do this by starting with a permissive DTO and then mapping it to a validated internal class e.g.

data class MessageDTO(
   val message: String?,
   val level: String?
)

data class Message(
   val message: String
   val level: Level
)

val validateMessage = Validation<MessageDTO> {
  message required {}
  level required { enum<System.Logger.Level>() }
}

fun MessageDTO.asMessage() {
  (validateMessage(this) as? Valid<MessageDTO>) ?: throw ValidationException(...)

   return Message(
       message!!
       System.Logger.Level.valueOf(level!!)
   )
}

That said you might want to have a look at something like medeia-validator which can validate while parsing using a provided json-schema.

andrewliles commented 2 years ago

Thanks for the suggestion Niklas