Is there any way to use ensure or with kotlinx ser...
# arrow
d
Is there any way to use ensure or with kotlinx serialization? I need to validate a ktor request, and I want to handle that cleanly... But my validation is on a value class being decoded by kotlinx serialization...
s
This is the pattern I've used with Ktor and KotlinX. 1. Receive the JSON,
call.receive<A>()
either
catch
and turn it into
JsonError
or letting the KotlinX exception be handled by Ktor itself. 2. Validate the individual params of the resulting intermediate DTO class. (in service layer). 3. After validation turn into proper domain object. Should turn this into
Either
validation pattern from 1.2.x, but if you've seen both my webinars with Anton then it should be quite easy to update the example. A PR would also be very welcomed, and I would be happy to guide you there in more details as well 😉 (Update
Validated
to
Either
, and use
Either.zipOrAccumulate
instead of
Validated.zip
)
d
Yeah, well I don't have an intermediate DTO... those are my service entry points, and I'm trying to ensure that they're correct... I guess a one idea would be to defer validation to after the encoding, but then I can't really put the logic into a value class constructor or init block, and I'd have to remember to put them whereever I need to use them (like in unit test code...). Another could be to throw exceptions and just Raise.catch them when using Json.decode... but then they won't be accumulated...
s
I think this is more a question about KotlinX Serialization, and not Arrow 🤔 It's not possible to inject validation into the KotlinX (de)serializers. So you're left with: • inspecting their exception, and trying to gather all info from there. • manually decoding. • DTO + validate after serialisation Otherwise I misunderstood your question.
d
Copy code
@Serializable
data class FooRequest(val baz: Baz,val bar: Bar)

@Serializable
@JvmInline
value class Baz(val value: Int) {
  init { require(value in 21..39) } 
}

@Serializable
@JvmInline
value class Bar(val value: Int) {
  init { require(value in 5..10) } 
}
This has validation logic INSIDE the value classes in a way that you can't deserialize something wrong... so in tests I don't have to remember to validate before (or take the chance of accidentally putting the system in a wrong state in that test). I'm wondering (if they work...) if I would have context receivers on those classes with
Raise<RequestError>
I could technically use
ensure
instead of
require
... but I guess that's not right now...
s
Even then I am not sure if you could make that co-operate with KotlinX Coroutines. Since you would need the accumulate behavior inside of the (de)serializer of
FooRequest
.
d
Oh... so just surrounding it
catch({ Json.decodeFromString<FooRequest>(json) }) {...}
wouldn't be enough?
s
Not in an accumulating way no. It would fail on
Baz
if it's value is not inside
21..39
and just result in that.
Because that's how the generated (de)serializers of KotlinX Serialization work.
d
The truth is that if
Raise.ensure
is being used instead of
require
in those init blocks, I wouldn't even need
catch
...
either { }
would be enough, unless you mean that inside KotlinX Serialization they loose the coroutine context and
ensure
wouldn't work properly...?
s
Oh, I'm sorry I thought you wanted to accumulate errors. If that is a not requirement I think it would indeed work correctly if you can put
Raise<RequestError>
on your
value class
. You would also still need to deal with
MissingFieldException
from KotlinX though. So it might be easier to write an utility function that deals with both the exceptions from
require
and
MissingFieldException
.
d
Interesting that that would work, but then there wouldn't be any advantage over regular
require
with a ``catch`... Yeah the point WAS to accumulate errors... but i guess I'm not understanding how validation works well enough I thought they automatically accumulate themselves in a
Left<NotEmptyList<RequestError>>
each time one calls
ensure...
?
s
That is not possible, since i.e.
ensureNotNull
statements can depend on each other. Here the second
ensure
depends on the result of the previous one, so it's not possible to accumulate.
Copy code
fun Raise<String>.example() {
  val x: Int? = ...
  ensureNotNull<Int>(x) { "fail-1" }
  val str = x.toString()
  ensure(str.size > 3) { "not long enough" }
}
To accumulate you need to explicitly use
xOrAccumulate
APIs,
zipOrAccumulate
or
Iterable.mapOrAccumulate
, etc.