Toshihiro Nakamura
12/04/2025, 12:54 AMand, or, map, chain)
• Clear, structured error messages with keys you can localize
• No reflection, simple JVM-first design
It’s still early days, but feedback is super welcome!
👉 GitHub: https://github.com/komapper/kova
Thanks!Youssef Shoaib [MOD]
12/04/2025, 1:36 AMprovideDelegate to enforce that the ability to register a property validator can only happen inside the class body.
Some integrations with Arrow could be nice to have! Arrow has Raise for signalling errors, and Accumulate for error accumulation. Interop back and forth between the 2 libraries may thus be a nice plus! (Ping/DM me or ask in #C5UPMM0A0 if you have any questions! I worked on the latest design for Accumulate, so I should be able to help!).
The "Arrow approach" is to get away from having Validators as first-class objects that get passed around and such, and instead represent a Validator using a function. Validator chaining then becomes normal function composition. Validation `or`ing just becomes a simple ||, etc. Error accumulation is done by only "forcing" an error to happen when a value is really needed. tryValidate et all then become DSL entry-points. This also means we can use inline lambdas everywhere, so it works well with suspend and even @Composable! It also makes the DSL compose nicely with other features, like `raise`ing an error in your validator if you encounter something fatal that should end validation, or ResourceScope for resource cleanup (although I struggle to see how that'd be relevant to validation, but it's possible nevertheless!). The difference here is analogous to the difference between a classic parser combinator library vs a suspend-based, function-based parsing library.
constrain might be nicer as vararg parameters to the constructor, instead of a lambda.
I don't see on the surface why type-specific validators are necessary, but you probably have a good reason for it. It just seems to me that Validator<String> might work just as well with extension functions, or even Validator<T> with T: Number as a replacement for NumberValidator<T>
I don't really get ObjectFactory. It seems to me easier to make the Person first then validate. This might be where an Arrow-like approach shines.Norbert Kiesel
12/04/2025, 5:35 AM.not() or e.g. a doesNotMatch(r: RegExp), because that makes it much easier to e.g. validate that a string does not contain "PO Box".Toshihiro Nakamura
12/04/2025, 12:31 PMI don’t see on the surface why type-specific validators are necessary,The reason I’m creating type-specific validators is that the operations I want to provide differ by type. For example, Validator<String> offers operations like uppercase(), while Validator<Int> does not.
I don’t really get ObjectFactory. It seems to me easier to make the Person first then validate.You’re absolutely right. Still, I have a motivation to keep domain models always in a valid state. For that reason, I’m experimenting with an approach where all arguments are validated before instantiating the object.
Toshihiro Nakamura
12/04/2025, 12:31 PMI would suggest to also either add a .not() or e.g. a doesNotMatch(r: RegExp)Thank you for the great suggestion. I’ll give it some serious consideration.
Youssef Shoaib [MOD]
12/04/2025, 6:07 PMFor example, Validator<String> offers operations like uppercase(), while Validator<Int> does not.but that's exactly my point. You can make extension functions on
Validator<String> instead of having a special StringValidator typeToshihiro Nakamura
12/04/2025, 10:00 PMValidator<String> and Validator<Int> are treated separately. Indeed, in the example I mentioned earlier, it seems that StringValidator would no longer be necessary.
However, in cases like the following where the signatures are identical, I believe a concrete class is needed to avoid a platform declaration clash.
fun interface Validator<T> {
fun execute(input: T): Boolean
}
fun Validator<String>.max(value: Int): Validator<String> = Validator {
it.length <= value
}
fun Validator<Int>.max(value: Int): Validator<Int> = Validator {
it <= value
}
val stringValidator = Validator<String> { true }
// platform declaration clash: The following declarations have the same JVM signature
stringValidator.max(10).execute("abc").let { println(it) }Youssef Shoaib [MOD]
12/04/2025, 10:23 PM@JvmName("maxString") to prevent such a clash. I think it can really cut down on the amount of classes you have, and hence make the library less complex and easier to maintainToshihiro Nakamura
12/04/2025, 10:55 PM@JvmName. I’ll give it a try. Thanks for letting me know!Youssef Shoaib [MOD]
12/05/2025, 1:12 AMfoo.kt generates a class FooKt)Toshihiro Nakamura
12/05/2025, 1:50 PMYoussef Shoaib [MOD]
12/06/2025, 4:25 AMconstraint writing incredibly simple, since there's no need to craft your own messages anymore, the compiler plugin takes care of it!Toshihiro Nakamura
12/06/2025, 10:13 AMYoussef Shoaib [MOD]
12/07/2025, 4:23 AMToshihiro Nakamura
12/07/2025, 3:10 PM