Hi there! I am currently tackling a rather common problem: when deserialising some JSON request in a web service, I need to be able to distinguish between an absent property in the request, and a property that was explicitly set to null
in the request.
There are several solutions to this problem, which broadly fall into two categories: 1) wrap the value in some custom type and write custom deserialisers, validation and other logic for this wrapper type, or 2) do some extra bookkeeping at the point you set(...)
the value, so you can track that it has indeed been set by the deserialising logic.
Due to the constraints I am working with, I can’t really write custom deserialisers, so I am restricted to option 2.
After some experimenting with biolerplaterrific solutions, I stumbled upon delegates, which so far seem to be a nice solution, but I’m stumbling “at the finish line”, by a rather ugly method signature I can’t seem to get rid of. It is possible I am running into the limits of the Java/Kotlin type system, but I’d like to know if there isn’t some clever trick I am missing.
Currently I have the following:
For the request body:
interface DeserializedRequestBody {
fun registerDeserialization(property: KProperty<*>)
fun hasDeserializedField(property: KProperty<*>): Boolean
}
abstract class DefaultDeserializedRequestBody : DeserializedRequestBody {
private val deserializedFields: MutableSet<KProperty<*>> = mutableSetOf()
override fun registerDeserialization(property: KProperty<*>) {
deserializedFields.add(property)
}
override fun hasDeserializedField(property: KProperty<*>) =
property in deserializedFields
}
For the DeserialzedFieldDelegate:
class DeserializedFieldDelegate<T, R : DeserializedRequestBody> {
private var backingField: T? = null
operator fun getValue(thisRef: R, prop: KProperty<*>): T? {
return backingField
}
operator fun setValue(thisRef: R, prop: KProperty<*>, value: T?) {
backingField = value
thisRef.registerDeserialization(prop)
}
}
And then I have this inline function:
inline fun <reified T : DeserializedRequestBody, V> T.deserializedValueOfOrDefault(
property: KProperty1<T, V>,
default: V?
): V? {
return if (this.hasDeserializedField(property)) {
property.get(this)
} else {
default
}
}
I can currently use these in the following way:
class FooRequestBody : DefaultDeserializedRequestBody() {
@get:Length(min = 0, max = 255)
var title: String? by DeserializedFieldDelegate()
var number: Int? by DeserializedFieldDelegate()
}
class Foo(
var title: String = "",
var number: Int = 0
)
class FooService {
fun updateFoo(oldFoo: Foo, request: FooRequestBody) {
val newFoo: Foo = Foo(
title = request.deserializedValueOfOrDefault(FooRequestBody::title, oldFoo.title)!!,
number = request.deserializedValueOfOrDefault(FooRequestBody::number, oldFoo.number)!!
)
// repository.save(newFoo)
}
}
The neat thing about this is that, as far as all the old, battle-tested java frameworks are concerned, those delegated fields are just ordinary Strings and Ints (by virtue of their getters and setters). As an example, there is the @get:Length(min = 0, max = 255)
validation annotation on the title
field in the request, and aside from needing the get:
bit mixed in there, that just works. Thanks, Kotlin.
However, making the perfect the enemy of the good is practically my middle name, and so I am still quite annoyed by this syntax:
title = request.deserializedValueOfOrDefault(FooRequestBody::title, oldFoo.title)!!
More specifically, the FooRequestBody::title
property passing as a function argument seems pretty ugly over here in our nicely encapsulated OO-land.
Ideally (imho, obviously), I could do something like this:
title = request.title.valueOfOrDefault(oldFoo.title)!!
However, I’ve been poking at delegates, extension functions and generics for a couple of days now, and I cannot seem to take this final hurdle:
- Creating an extension
inline fun <reified T> SomeDelegate<T>.someFunction(bar: Bar)
does not permit me to callsomeFunction
on avalue: T by SomeDelegate()
property.value
is apparently never aSomeDelegate
, it is always aT
- Creating an extension function
fun Any.someFunction(bar: Bar)
and then somehow using reflection to check at runtime if the called on property is actually delegated by… honestly I don’t even know how this sentence would end, it just feels wrong and stupid and I will end up creating Spring if I continue on this path. - Actually do have e.g.
title
be of someDeserializationWrapper<String>
type, and then somehow making it so (through a different kind of delegate?) that I can just read and writeString
s to it… I’ve been toying with theprovideDelegate
function to try and compute a delegate for my wrapper that somehow allows it to read and write values of the wrapped types transparantly, while also being able to have extra functionality on the wrapper… No success so far, and explaining it like this makes it sound like I want to have a value that is two types simultaneously… which kinda sums it up I guess But only in a syntactic-sugar kind of way so I’m not sure if it is actually definitely impossible…
Any hints or insights very much appreciated!