meshBlog
JSR-363 Units of Measurement API in Practice – JSON Serialization
In the last post we looked at how to persist JSR-363 types like Quantity<q>
with Spring Data using a pair of converters to serialize a Quantity
to string and back. In this post we will look at how to serialize Quantity
to JSON using a similar trick. This allows us to use Quantities and Units in a REST API.
The JacksonQuantityModule
Jackson offers the module SPI to allow consumers to register various hooks for JSON serialization and deserialization. For example, the Jackson Kotlin module uses this mechanism to help Jackson better serialize Kotlin specific types like Pair<T, U>
.
So here's our JacksonQuantityModule
:
@Suppress("unused")
class JacksonQuantityModule : SimpleModule(PackageVersion.VERSION) {
companion object {
private val serialVersionUID = 1L
}
override fun setupModule(context: SetupContext) {
addSerializer(Quantity::class.java, QuantitySerializer)
addDeserializer(Quantity::class.java, QuantityDeserializer)
super.setupModule(context)
}
object QuantitySerializer : JsonSerializer<Quantity<*>>() {
override fun handledType()API: Class<Quantity<*>> {
return Quantity::class.java
}
override fun serialize(value: Quantity<*>, gen: JsonGenerator, serializers: SerializerProvider?) {
val formatted = QuantityFormatting.QuantityToStringConverter.convert(value)
gen.writeString(formatted)
}
}
object QuantityDeserializer : JsonDeserializer<Quantity<*>>() {
override fun handledType(): Class<Quantity<*>> {
return Quantity::class.java
}
override fun deserialize(p: JsonParser, ctxt: DeserializationContext?): Quantity<*>? {
val source = p.valueAsString
return QuantityFormatting.StringToQuantityConverter.convert(source)
}
}
}
If you want to auto-register this module with Jackson through the SPI mechanism, just add a text file as a resource at META-INF/services/com.fasterxml.jackson.databind.Module
with the fully qualified classname of our module as its content:
my.package.JacksonQuantityModule
Unit Tests for JacksonQuantityModule
Let's add some quick unit tests for this module to verify SPI auto-registration and the module implementation work as expected:
val sut = ObjectMapper().apply {
findAndRegisterModules()
}
@Test
fun handlesQuantities() {
verify(ObjectWithQuantity(1.mega.byte), """{"q":"1 MBy"}""")
}
@Test
fun handlesNullQuantities() {
verify(ObjectWithQuantity(null), """{"q":null}""")
}
private inline fun verify(obj: T, expectedJson: String) {
val json = sut.writeValueAsString(obj)
Assert.assertEquals(expectedJson, json)
val result = sut.readValue(json)
Assert.assertEquals(obj, result)
}
Both tests pass, mission accomplished.