Different JSON views from a single source

Getting data in JSON format via REST services from the backend server is common practice. In the simplest case a JSON provider like Jackson translates your Java objects into a JSON string and back into Java objects automatically.

However, this does not cover cases where the data model (e.g., implemented as JPA entities) is different from the view model. For example, if you have BLOBs in your model it does not make much sense to transfer them as BASE64 encoded strings. Mostly, because BLOBs tend to be large and may not be needed at once.

In this article we will show how to provide different JSON “views” or dialects of the data using the same REST service.

Data model

Consider a data model that contains documents holding as well binary data as descriptions and a MIME type:

@Entity
public class Document extends AbstractEntity
{
  @Convert(converter = MediaTypeConverter.class)
  @Column(name = "CONTENT_TYPE", length = 50, nullable = false)
  private MediaType contentType = MediaType.valueOf("image/jpg");

  @Column(name = "NAME", length = 512, nullable = false)
  private String name = "";

  @Lob  @Basic(fetch = FetchType.EAGER)
  @Column(name = "DATA")
  private byte[] data;
  ...
}

In our example we provide two slightly different views of a document: One for a JavaScript based web frontend, where we replace the real data with a REST-URL pointing to the real data. Thus the web frontend may decide to load the data later on.
The other view uses the default behavior, i.e. the data will be encoded as a BASE64 string.

REST service

The service just loads the document and returns it. The transformation into JSON is performed by JAX-RS and Jackson behind the scenes.

@GET @Path(.../{id}")
public Document load(@PathParam("id") final String id,                                                
                     @QueryParam("view") final String viewName)
{
  final Document result = dao.load(id)
          .orElseThrow(() -> new NotFoundException("Document with ID '" + id + "' not found"));
  viewProvider.setView(viewName);

  return result;
}

The view provider is a simple data holder class:

@RequestScoped
public class ViewProvider
{
  private Class<?> view = WebView.class;
  public Class<?> getView() { return view; }
  public void setView(final String viewName)
  {
    view = "java".equalsIgnoreCase(viewName) ? JavaView.class : WebView.class;
  }
  ...
}

JSON serializer

The serializer (and the according deserializer, omitted for brevity) uses the view information to decide about the format:

public class DokumentSerializer extends StdSerializer<Dokument>
{
  @Inject
  private ViewProvider viewProvider;

  @Override
  public void serialize(final Document value, final JsonGenerator jgen, final SerializerProvider provider) throws IOException
  {
    final String id = value.getId();
    jgen.writeStartObject();
    jgen.writeStringField("id", id);
    jgen.writeStringField("name", value.getName());

    final String data = (viewProvider.getView() == WebView.class)?
            createUriToDocument(id) :
            Base64.getEncoder().encodeToString(value.getData());
    
    jgen.writeStringField("data", data);
    jgen.writeStringField("contentType", value.getContentType().toString());
    jgen.writeEndObject();
  }
}

Plug everything together

In order to use our serializer instead of the default one we supply a module to the Jackson object mapper used by JAX-RS:

@Provider
public class ObjectMapperContextResolver implements ContextResolver<ObjectMapper>
{
  @Inject
  private DocumentModule module;

  @Inject
  private ViewProvider viewProvider;

  @Override
  public ObjectMapper getContext(final Class<?> type)
  {
    final ObjectMapper objectMapper = new ObjectMapper();
    final Class<?> view = viewProvider.getView();
    return objectMapper.setConfig(objectMapper.getSerializationConfig().withView(view))
            .setConfig(objectMapper.getDeserializationConfig().withView(view))
            .registerModule(module);
  }
}

Test

To test the result you just have to supply a query parameter named “view”:

Conclusion

The described approach has been developed on a WildFly application server but should  in principle work for any framework that supports JAX-RS.

Alternate approaches

Another solution might be to provide a REST service that uses a Response object to construct the differing answers. However, this is awkward and requires to obtain an JSON-ObjectMapper on your own.

You can also use JSON views without a custom serializer/deserializer using the @JsonView annotation. But this way the annotated property is just omitted, not transformed differently.

Posted in All