I'm using Jackson with Kotlin to serialize and deserialize a variety of Kotlin data class
es.
My data class
es are quite simple objects which can easily be serialized and deserialized using the standard Jackson ObjectMapper
, except that I want to ensure that some post-validation is done as part of the deserialization. For example, I want to ensure that Thing.someField >= 0
:
data class InnerThing(
val foo: String
);
data class Thing(
val someField: Int, // must not be negative
val innerThing: InnerThing
);
The simplest way to implement this would seem to be to override the StdDeserializer
for the class(es) in question.
class ThingDeserializer : StdDeserializer<Thing>(Thing::class.java) {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Thing {
// Defer to the superclass to do the actual deserialization
// DOES NOT WORK because StdDeserializer.deserialize() is abstract
val t: Thing = super.deserialize(p, ctxt);
if (thing.someField < 0) {
throw RuntimeException("someField value must be >= 0");
}
return t;
}
}
… but that does not work because StdDeserializer.deserialize()
is abstract. Which leads me to this related question. It appears that it's amazingly difficult to defer to the default deserialization behavior from within a custom deserializer.
From what I can tell, the most straightforward way to defer to the default deserialization behavior is to create an entirely separate ObjectMapper
(!!!), use that to read the class, and then do the post-validation.
Leading me to this…
class ThingDeserializer : StdDeserializer<Thing>(Thing::class.java) {
// Create a whole separate ObjectMapper that doesn't override
// the deserializer for Thing:
private val defaultMapper = jacksonObjectMapper();
init {
// Need to reregister modules for things like ISO8601
// timestamp parsing:
defaultMapper.registerModule(JavaTimeModule());
}
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Thing {
// Use the non-overridden ObjectMapper to deserialize the object:
val t: Thing = defaultMapper.readValue(p, Thing::class.java);
// ... and then do validation afterwards
if (thing.someField < 0) {
throw RuntimeException("someField value must be >= 0");
}
return t;
}
}
fun main(args: Array<String>) {
val mapper = jacksonObjectMapper();
mapper.registerModule(JavaTimeModule());
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
mapper.registerModule(
SimpleModule().apply {
addDeserializer(Thing::class.java, ThingDeserializer())
}
);
val test: Thing = mapper.readValue("""{
"someField": -1,
"innerThing": { "foo": "bar" }
}""");
}
This does work; it throws a RuntimeException
for the unwanted value of someField
in the test case.
This seems wrong
Is the above really a reasonable way to do this validation?
It has a very bad code smell. I'm creating a whole new ObjectMapper
, and I have to re-register modules for things like timestamp deserialization.
Question
It feels like there must be a saner, less redundant way to deserialize a Kotlin data class
(or Java POJO) and then do post-validation of the contents of the object, without rewriting the deserialization machinery entirely.
Is there? I can't figure out a simple way to defer to the default deserialization behavior of ObjectMapper
from within the deserializer.