Jackson Bitrate Support with JSR 385

Many interesting libraries are created through the Java Specification Requests process. Many of them are incorporated directly into the JDK, but some exist as independent libraries.

Some time ago I wrote about the Moneta library of the JRS 354 (Java Money) reference implementation. This article is about a use case for Indriya, the JSR 385 (Units of Measurement API) reference implementation.

The JDK does not include a data type to create and manipulate values in SI quantities such as speed, distance, time, weight, temperature, etc. However, this is possible with the Indriya library. To use it, your own project requires the following dependency.

<dependency>
  <groupId>tech.units</groupId>
  <artifactId>indriya</artifactId>
  <version>2.2</version>
</dependency>

Once the library has been integrated, quantities of various sizes can be generated.

Quantity<Speed> speedInMps = Quantities.getQuantity(30, Units.METRE_PER_SECOND);
Quantity<Speed> speedInKmh = Quantities.getQuantity(18.52, Units.KILOMETRE_PER_HOUR);
Quantity<Speed> converted = speedInMps.to(Units.KILOMETRE_PER_HOUR));

In the example above, two quantities are generated for speed. The first with the amount 30 and the unit \frac m s and the second with the amount 18.52 and the unit km/h. The third quantity corresponds to the first, but the amount has been converted to the unit km/h. The Units class contains a large number of additional units for a variety of use cases.

Quantity<Speed> speedSum = speedInMps.add(speedInKmh);

double knots = speedInMps.getValue().doubleValue() * 3600 / 1852;

As you can see in this example, you can also calculate with the quantities. In this example, the second quantity is added to the first in the first line and the amount of the first quantity is multiplied by \frac{3600} {1852} in the second line. This calculation gives us the amount in knots instead of \frac m s.

One advantage of calculating with quantities is the reduction of errors. When using quantities, it is not possible to accidentally add up amounts of speed and weight.

However, a library for units of measurement would only be half as interesting if you could not create your own units and could only use the standard units from the Units class. In the example below, we create our own KNOT unit, which is derived from the METRE_PER_SECOND unit.

Unit<Speed> KNOT = new TransformedUnit<>("kn", "Knot", Units.METRE_PER_SECOND, MultiplyConverter.ofRational(1852, 3600));
SimpleUnitFormat.getInstance().label(KNOT, "kn");

Number kmh = Quantities.getQuantity("1 kn").asType(Speed.class)
  .to(Units.KILOMETRE_PER_HOUR).getValue();

Quantities with the KNOT unit can be used like all other quantities of speed in calculations and conversions.

However, this article is not about the calculation with knots but about the transfer of data in bit rate with Jackson.

If information on bit rates is transported via REST interfaces, it is either numbers without units or a string containing a unit. In the following example, we see a JSON artifact that contains two bit rates. The attributes upload with 50 Gbit/s and download with 50 Gbit/s.

{
  "name": "DG basic 100", 
  "upload": "50 Gbit/s", 
  "download": "100 Gbit/s"
}

Without a proper agreement on the API, however, this can lead to misunderstandings and instead of a Gbit/s, a Mbit/s is specified.

{
  "name": "DG basic 100", 
  "upload": "50000 Mbit/s", 
  "download": "100000 Mbit/s"
}

Of course, this should not lead to any problems if the developers of the API verify the units correctly. For all others, here is a solution with JSR 385.

So that we can define our bit rates properly, we first need two new quantities: Information and Bitrate. The quantity Information is not technically necessary, but with it the definition is nicer.

public interface Information extends Quantity<Information> {
}

public interface Bitrate extends Quantity<Bitrate> {
}

Similar to the Units class in the reference implementation, we create our own BitrateUnits class. It contains all the Unit constants that we want to use later.

public class BitrateUnits extends AbstractSystemOfUnits {
  private static final Unit<Information> BIT = addUnit(new BaseUnit<>("bit", "Bit", UnitDimension.NONE));
  public static final Unit<Bitrate> BITS_PER_S = getBandwidthProductUnit(null);
  public static final Unit<Bitrate> KILOBITS_PER_S = getBandwidthProductUnit(MetricPrefix.KILO);
  public static final Unit<Bitrate> MEGABITS_PER_S = getBandwidthProductUnit(MetricPrefix.MEGA);
  public static final Unit<Bitrate> GIGABITS_PER_S = getBandwidthProductUnit(MetricPrefix.GIGA);
  public static final Unit<Bitrate> TERABITS_PER_S = getBandwidthProductUnit(MetricPrefix.TERA);

  private static ProductUnit<Bitrate> getBandwidthProductUnit(Prefix prefix) {
    Unit<Information> informationUnit = prefix == null ? BitrateUnits.BIT : BitrateUnits.BIT.prefix(prefix);
    ProductUnit<Bitrate> unit = new ProductUnit<>(informationUnit.divide(Units.SECOND));
    SimpleUnitFormat.getInstance().label(unit, prefix == null ? "bit/s" : prefix.getSymbol() + "bit/s");
    return unit;
  }

  // ...
}

The first dimension we define is BIT as an information unit. All other units are ProductUnit instances, which are variants of BIT divided by SECOND.

With these definitions, we can instantiate the two bit rates from the example.

Quantity<Bitrate> upload = Quantities.getQuantity(50, BitrateUnits.GIGABITS_PER_S);
Quantity<Bitrate> download = Quantities.getQuantity(100, BitrateUnits.GIGABITS_PER_S);

These two quantities can now be manipulated and converted.

assertEquals(150000, upload.add(download).to(BitrateUnits.MEGABITS_PER_S).getValue());

Here, both bit rates are added together and the result is converted to Mbit/s.

To use quantities in Jackson, we need a custom Serializer and Deserializer. To keep the Jackson configuration simple, we provide both in one Module.

public class BitrateModule extends SimpleModule {
  @Override
  public void setupModule(SetupContext context) {
    context.addSerializers(new SimpleSerializers(List.of(new StdSerializer<Quantity<Bitrate>>(ReferenceType.upgradeFrom(SimpleType.constructUnsafe(Quantity.class), SimpleType.constructUnsafe(Bitrate.class))) {
      @Override
      public void serialize(Quantity<Bitrate> quantity, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
        jsonGenerator.writeString(quantity.toString());
      }
    })));
    context.addDeserializers(new SimpleDeserializers(Map.of(Quantity.class, new StdDeserializer<Quantity<Bitrate>>(Quantity.class) {
      @Override
      @SuppressWarnings("unchecked")
      public Quantity<Bitrate> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
        try {
          return (Quantity<Bitrate>) Quantities.getQuantity(jsonParser.getValueAsString());
        } catch (IllegalArgumentException e) {
          return (Quantity<Bitrate>) deserializationContext.handleWeirdStringValue(_valueClass, jsonParser.getValueAsString(),
            "not a valid representation (error: %s)", ClassUtil.exceptionMessage(e));
        }
      }
    })));
  }
}

The Serializer writes the Quantity to the JSON output using its toString method. The Deserializer reads the Quantity from the String attribute using Quantities.getQuantity.

With this implementation, the Jackson ObjectMapper can already work successfully on bit rates, as the following example shows.

ObjectMapper objectMapper = new ObjectMapper()
               .enable(SerializationFeature.INDENT_OUTPUT)
               .registerModule(new BitrateModule());

Product product = new Product("DG basic 100", upload, download);

String json = objectMapper.writeValueAsString(product);

assertEquals("""
                {
                  "name" : "DG basic 100",
                  "upload" : "50 Gbit/s",
                  "download" : "100 Gbit/s"
                }""", json.replace("\r", ""));

Product value = objectMapper.readValue(json, Product.class);

assertEquals(50000, value.upload().to(BitrateUnits.MEGABITS_PER_S).getValue());
assertEquals(100000, value.download().to(BitrateUnits.MEGABITS_PER_S).getValue());

The ObjectMapper with the BitrateModule writes a Product instance with bit rates into the desired JSON and this JSON can be read back sucessfully by the ObjectMapper.

If you want to try out the Bitrates with your ObjectMapper, you can find the corresponding project on Maven Central.

<dependency>
  <groupId>de.schegge</groupId>
  <artifactId>jackson-bitrate-support</artifactId>
  <version>0.1.0</version>
</dependency>

Leave a Comment