HTTP Content Negotiation

Ever had the situation where you’ve implemented a REST API, and then another team or potential customer comes along and asks if you could just change the serialisation format? Or maybe you are using a textual media type and want to experiment to see if performance would improve with a binary format? I see this done wrong all the time.

There is a mechanism for content negotiation built in to HTTP since the early days. It’s a very simple mechanism so this won’t be a long post. There is a short Java example using Jackson and Jersey to allow parametric content negotiation to be performed transparently to both the server and client.

Never Ever Do This

One option would be to implement a completely new service layer for each content type you need to serve. This not only costs you time, and probably test coverage, but moving between serialisation formats as a client of your API will probably constitute a full-blown migration. Keeping the implementations synchronised will become difficult, and somebody needs to tell the new guy he needs to implement the change in the JSON version as well as the SMILE version, and if the XML version could be updated by the end of the week, that would be great. I have seen commercial products that have actually done this.

Don’t Do This (Unless You’ve Already Done It)

Another option that I see in various APIs is using a query parameter for GET requests. This offends my sensibilities slightly, because it has the potential to conflate application and transport concerns, but it’s not that bad. The SOLR REST API has a query parameter wt which allows the client to choose between JSON and XML, for instance. It’s not exactly cumbersome and the client can easily change the serialisation format by treating it as a concern of the application. But what if you want to query the API from a browser? Browsers speak HTTP ex cathedra, not the local patois your API uses. The browser will have to put up with whatever your defaults are, and hope it can consume that format. It’s unnecessary complexity in the surface area of your API but it’s quite common, and at least it isn’t weird.

1996 Calling

All you have to do is set the Accept header of the request to the media type (for instance – application/cbor) you want to consume in the response.  If the server can produce the media type, it will do so, and then set the Content-Type header on the response. If it can’t produce the media type (and this should come out in integration testing) the server will respond with a 300 response code with textual content listing the supported media types that the client should choose from.

Content Negotiation with Jersey and Jackson JAX-RS providers

Jackson makes this very easy to do in Java. We want to be able to serialise the response below into a MIME type of the client’s choosing – they can choose from JSON, XML, CBOR, SMILE and YAML. The response type produced is not constrained by annotations on the method; none of the formats need ever be formally acknowledged by the programmer and can be added or removed by modifying the application’s classpath.

@Path("content")
public class ContentResource {

  @GET
  public Response getContent() {
    Map<String, Object> content = new HashMap<>();
    content.put("property1", "value1");
    content.put("property2", 10);
    return Response.ok(content).build();
  }
}

There are Jackson JAX-RS providers included by the maven dependencies below. Their presence is transparent to the application and can be used via the standard JAX-RS Resource SPI. Handler resolution is implemented by a chain-of-responsibility where the first resource accepting the Accept header will serialise the body of the response and set the Content-Type header.

        <dependency>
            <groupId>com.fasterxml.jackson.jaxrs</groupId>
            <artifactId>jackson-jaxrs-json-provider</artifactId>
            <version>2.8.7</version>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.jaxrs</groupId>
            <artifactId>jackson-jaxrs-xml-provider</artifactId>
            <version>2.8.7</version>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.jaxrs</groupId>
            <artifactId>jackson-jaxrs-smile-provider</artifactId>
            <version>2.8.7</version>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.jaxrs</groupId>
            <artifactId>jackson-jaxrs-cbor-provider</artifactId>
            <version>2.8.7</version>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.jaxrs</groupId>
            <artifactId>jackson-jaxrs-yaml-provider</artifactId>
            <version>2.8.7</version>
        </dependency>

There’s some simple coding to do to configure the Jersey resource and run it embedded with Jetty.

public class ServerRunner {

  public static void main(String[] args) throws Exception {
    ResourceConfig config = new ResourceConfig();
    config.packages(true, "com.fasterxml.jackson.jaxrs");
    config.register(ContentResource.class);
    ServletHolder servletHolder = new ServletHolder(new ServletContainer(config));
    Server server = new Server(8080);
    ServletContextHandler handler = new ServletContextHandler(server, "/*");
    handler.addServlet(servletHolder, "/*");
    server.start();
    server.join();
  }
}

If you do this, then you can leave the decision up to your client and just add and remove resource provider jars from your classpath. A silver lining is that you can test the mechanism yourself from IntelliJ:

accept

response

And you can get non-technical users to check from a browser:

broswer.PNG

Concise Binary Object Representation

Concise Binary Object Representation (CBOR) defined by RFC 7049 is a binary, typed, self describing serialisation format. In contrast with JSON, it is binary and distinguishes between different sizes of primitive type properly. In contrast with Avro and Protobuf, it is self describing and can be used without a schema. It goes without saying for all binary formats: in cases where data is overwhelmingly numeric, both parsing time and storage size are far superior to JSON. For textual data, payloads are also typically smaller with CBOR.

The Type Byte

The first byte of every value denotes a type. The most significant three bits denote the major type (for instance byte array, unsigned integer). The last five bits of the first byte denote a minor type (float32, int64 and so on.) This is useful for type inference and validation. For instance, if you wanted to save a BLOB into HBase and map that BLOB to a spark SQL Row, you can map the first byte of each field value to a Spark DataType. If you adopt a schema on read approach, you can validate the supplied schema against the type encoding in the CBOR encoded blobs. The major types and some interesting minor types are enumerated below but see the definitions for more information.

  • 0:  unsigned integers
  • 1:  negative integers
  • 2:  byte strings, terminated by 7_31
  • 3:  UTF-8 text, terminated by 7_31
  • 4:  arrays, terminated by 7_31
  • 5:  maps, terminated by 7_31
  • 6:  tags, (0: timestamp strings, 1: unix epoch longs, 2: big integers…)
  • 7:  floating-point numbers, simple ubiquitous values (20: False, 21: True, 22: Null, 23: Undefined, 26: float, 27: double, 31: stop byte for indefinite length fields (maps, arrays etc.))

Usage

In Java, CBOR is supported by Jackson and can be used as if it is JSON. It is available in

<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-cbor</artifactId>
    <version>2.8.4</version>
</dependency>

Wherever you would use an ObjectMapper to work with JSON, just use an ObjectMapper with a CBORFactory instead of the default JSONFactory.

ObjectMapper mapper = new ObjectMapper(new CBORFactory());

Jackson integrates CBOR into JAX-RS seamlessly via

<dependency>
    <groupId>com.fasterxml.jackson.jaxrs</groupId>
    <artifactId>jackson-jaxrs-cbor-provider</artifactId>
    <version>2.8.4</version>
</dependency>

If a JacksonCBORProvider is registered in a Jersey ResourceConfig (a one-liner), then any resource method annotated as @Produces("application/cbor"), or any HTTP request with the Accept header set to “application/cbor” will automatically serialise the response as CBOR.

Jackson deviates from the specification slightly by promoting floats to doubles (despite parsing floats properly it post-processes them as doubles), Jackson recognises floats properly as of 2.8.6 and distinguishes between longs and ints correctly so long as CBORGenerator.Feature.WRITE_MINIMAL_INTS is disabled on the writer.

In javascript, cbor.js can be used to deserialise CBOR, though loss of browser native support for parsing is a concern. It would be interesting to see some benchmarks for typical workloads to evaluate the balance of the cost of javascript parsing versus the benefits of reduced server side cost of generation and reduced message size. Again, for large quantities of numeric data this is more likely to be worthwhile than with text.

Comparison with JSON – Message Size

Textual data is slightly smaller when represented as CBOR as opposed to JSON. Given the interoperability that comes with JSON, it is unlikely to be worth using CBOR over JSON for reduced message size.

Large arrays of doubles are a lot smaller in CBOR. Interestingly, large arrays of small integers may actually be smaller as text than as binary; it takes only two bytes to represent 10 as text, whereas it takes four bytes in binary. Outside of the range of -99 to 999 this is no longer true, but might be a worthwhile economy for large quantities of survey results.

JSON and CBOR message sizes for messages containing mostly textual, mostly integral and mostly floating point data are benchmarked for message size at github. The output is as follows:

CBOR, Integers: size=15122B
JSON, Integers: size=6132B
CBOR, Doubles: size=27122B
JSON, Doubles: size=54621B
CBOR, Text: size=88229B
JSON, Text: size=116565B

Comparison with JSON – Read/Write Performance

Using Jackson to benchmark the size of the messages is not really a concern since it implements each specification; the output and therefore size should have been the same no matter which library produced the messages. Measuring read/write performance of a specification is difficult because only the implementation can be measured. It may well be the case that either JSON or CBOR can be read and written faster by another implementation than Jackson (though I expect Jackson is probably the fastest for either format). In any case, measuring Jackson CBOR against Jackson JSON seems fair. I benchmarked JSON vs CBOR writes using the Jackson implementations of each format and JMH. The code for the benchmark is at github

The results are as below. CBOR has significantly higher throughput for both read and write.

Benchmark Mode Count Score Error Units
readDoubleDataCBOR thrpt 5 12.230 ±1.490 ops/ms
readDoubleDataJSON thrpt 5 0.913 ±0.046 ops/ms
readIntDataCBOR thrpt 5 16.033 ±3.185 ops/ms
readIntDataJSON thrpt 5 8.400 ±1.219 ops/ms
readTextDataCBOR thrpt 5 15.736 ±3.729 ops/ms
readTextDataJSON thrpt 5 1.065 ±0.026 ops/ms
writeDoubleDataCBOR thrpt 5 26.222 ±0.779 ops/ms
writeDoubleDataJSON thrpt 5 0.930 ±0.022 ops/ms
writeIntDataCBOR thrpt 5 31.095 ±2.116 ops/ms
writeIntDataJSON thrpt 5 33.512 ±9.088 ops/ms
writeTextDataCBOR thrpt 5 31.338 ±4.519 ops/ms
writeTextDataJSON thrpt 5 1.509 ±0.245 ops/ms
readDoubleDataCBOR avgt 5 0.078 ±0.003 ms/op
readDoubleDataJSON avgt 5 1.123 ±0.108 ms/op
readIntDataCBOR avgt 5 0.062 ±0.008 ms/op
readIntDataJSON avgt 5 0.113 ±0.012 ms/op
readTextDataCBOR avgt 5 0.058 ±0.007 ms/op
readTextDataJSON avgt 5 0.913 ±0.240 ms/op
writeDoubleDataCBOR avgt 5 0.038 ±0.004 ms/op
writeDoubleDataJSON avgt 5 1.100 ±0.059 ms/op
writeIntDataCBOR avgt 5 0.031 ±0.002 ms/op
writeIntDataJSON avgt 5 0.029 ±0.004 ms/op
writeTextDataCBOR avgt 5 0.032 ±0.003 ms/op
writeTextDataJSON avgt 5 0.676 ±0.044 ms/op

The varying performance characteristics of media types/serialisation formats based on the predominant data type in a message make proper HTTP content negotiation important. It cannot be known in advance when writing a server application what the best content type is, and it should be left open to the client to decide.