Explore the idea of attaching custom details to ConstraintViolation
The ConstraintViolation has path and message, which is sufficient for most cases.
But it is not the most straightforward design if I need to pinpoint on the exact constraint being violated and act on it. For instance, to pick out a particular constraint violation and throw an alternate Exception instead of the generic Exception.
Currently, to workaround this limitation, there are possibly two ways:
- Override the
pathusing thewithPath { absolute("some_identifier") }DSL - Override the
messageusing theotherwise { "some_identifier" }DSL
The first workaround loses the path in a meaningful way. And the second workaround loses human readable messages.
Is it possible to add a third field of metadata: Map<String, Any> to ConstraintViolation, so that user can freely attach whatever metadata they need to the violation instance.
Possible DSL design:
val validate = Validator<Data> {
fieldOne.apply {
shouldNotBeEmpty()
constraint { it.meetsCustomCondition() } withMetadata { mapOf("key" to "customErrorKey1") }
}
}
Then, when analyzing the ConstraintViolation:
fun handleViolation(cv: ConstraintViolation) {
if (cv.metadata["key"] == "customErrorKey1")
throw CustomError(cv.message)
throw FallbackError(cv.message)
}
This is clearly an issue I want to address!
I think your idea of metadata is good. We could even add a dedicated code property to ConstraintViolation, because this is definitely something a lot of people could use.
Let me think about it, but I love the idea :)
I agree that we need more information in ConstraintViolation.
In valiktor the ConstraintViolation had all the information of the violation, such as the constraint with a message key and message parameters and the value that was not validated. This helps a lot when you want to sent a localised message or you want to send in JSON all the information to the UI and construct the error message there.
To make the custom details more flexible, it may be better to make the metadata a generic type, while also providing a default implementation or type alias that fix the type parameter to be a Map<String, String> or Map<String, Any>