meshBlog

JSR-363 Units of Measurement API in Practice – JSON Serialization

By Johannes Rudolph21. November 2017

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.