Fixing @JsonTypeInfo with Response and generics erasure issues

Goal

To properly serialize data with @JsonTypeInfo when using javax.ws.rs.core.Response

Description

In my last project, I had to work with an abstract (JPA) entity that had subtypes and to serialize them as JSON objects in a REST services API.

At first, the method that returned the list of entities was simply returning a generic List in a generic implementation such as the following:

public abstract class RestEntityController
  <     T extends DomainObject,      S extends BusinessService   > {

  @Inject
  private S businessService;

  protected S getBusinessService() {
    return businessService;
  }

  @GET
  public List<T> findEntities(
    @DefaultValue("0") @QueryParam("start") final int start,
    @DefaultValue("20") @QueryParam("count") final int count,
    @DefaultValue("id") @QueryParam("sortBy") final String sortBy,
    @DefaultValue("true") @QueryParam("asc") final boolean asc) {

    final Pager pager = new Pager();
    pager.setStart(start);

    pager.setCount(count);
    pager.addSorter(new Sorter(sortBy, asc));

    return businessService.findRange(pager);
  }

  ...
}

@Path("entities")
public class AbstractEntityRestController extends
  RestEntityController<AbstractEntity, AbstractEntityService> {
  ...
}

@Entity
@Table(name = "ENTITY")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "INSTANCE_TYPE")
@JsonTypeInfo(use = Id.CLASS, property = "@class")
public abstract class AbstractEntity extends DomainObject {
  ...
}

With the previous implementation, the result was similar to the following:

[
  {
    "@class": "com.linkare.myproject.ConcreteEntity",
    "id": 1,
    ...
  },
  ...
]

Later, the previous implementation was replaced by one that returned a javax.ws.rs.core.Response instead of the List of entities because we wanted to additionally return some headers related to pagination, namely the number of existing entities. After doing that change, the @class attribute was no longer shown and after some research, I realized that the problem was due to Java erasure and that we needed to do a different implementation. After several tests, this recipe summarizes the (best?) solution I came up with.

How to

The first thing was changing the implementation of the method that returned the list of entities with a Response:

public abstract class RestEntityController
  <     T extends DomainObject,      S extends BusinessService   > {

  ...
  /**
   * Defines the start of the paging
   */
  public static final String X_PAGINATION_PAGE = "X-Pagination-Page";

  /**
   * Defines the number of returned records
   */
  public static final String X_PAGINATION_LIMIT = "X-Pagination-Limit";

  /**
   * Defines the total count
   */
  public static final String X_PAGINATION_COUNT = "X-Pagination-Count";

  @GET
  public Response findRange(
    @DefaultValue("0") @QueryParam("start") final int start,
    @DefaultValue("20") @QueryParam("count") final int count,
    @DefaultValue("id") @QueryParam("sortBy") final String sortBy,
    @DefaultValue("true") @QueryParam("asc") final boolean asc,
    @DefaultValue("true") @QueryParam("pagingInfo") final boolean pagingInfo) {
    final Pager pager = new Pager();
    pager.setStart(start);

    pager.setCount(count);
    pager.addSorter(new Sorter(sortBy, asc));

    final List<T> results = businessService.findRange(pager);
    final ResponseBuilder responseBuilder = Response.ok().
      entity(getGenericEntityForListOf(results));

    if (pagingInfo) {
      final long totalCount = businessService.count();
      appendPagingInfo(responseBuilder, start, count, totalCount);
    }
    return responseBuilder.build();
  }

  protected void appendPagingInfo(final ResponseBuilder responseBuilder, final int start, final int count, final long totalCount) {
    responseBuilder.header(X_PAGINATION_PAGE, start);
    responseBuilder.header(X_PAGINATION_LIMIT, count);
    responseBuilder.header(X_PAGINATION_COUNT, totalCount);
  }

  /**
   *
   * @param results
   * @return the generic entity instance for the list of results passed in. This method is important to avoid issues with serialization of classes that
   *         use @JsonTypeInfo. Due to Java erasure, without this method and an overridden version that specifies exactly the class that should be used and
   *         that contains
   */
  protected GenericEntity<List<T>> getGenericEntityForListOf(final List<T> results) {
    return new GenericEntity<List<T>>(results) {
    };
  }
  ...
}

Notice the inclusion of a new query param named pagingInfo, with true as default value, to control if the pagination headers should be returned.

Notice also the inclusion of the method getGenericEntityForListOf (this is the important part in this recipe!) that enables us to specify the instance of the generic entity that should be used at runtime so that the JSON serialization process may know about what class should be serialized. That method is marked as protected on purpose so that it is overridden in the AbstractEntityRestController, returning a specific implementation of GenericEntity for List.

Finally, although not very important for this recipe, notice also the appendPagingInfo method that will set the pagination headers necessary on the returned response.

Next, we need to override the method getGenericEntityForListOf on our specific REST controller that deals with AbstractEntity (we don’t need to override the method on any other of our REST controllers that don’t deal with classes with subtypes):

@Path("entities")
public class AbstractEntityRestController extends
  RestEntityController<AbstractEntity, AbstractEntityService> {

  @Override
  protected GenericEntity<List<AbstractEntity>> getGenericEntityForListOf(final List<AbstractEntity> results) {
    return new AbstractEntityListGenericEntity(results);
  }

  ...
}

The specific REST class will return an instance of AbstractEntityListGenericEntity, which is a concrete implementation of GenericEntity parametrized with our AbstractEntity. That implementation is as simple as:

public class AbstractEntityListGenericEntity extends
  GenericEntity<List<AbstractEntity>> {

  /**
   * @param entity
   * @param genericType
   */
  public AbstractEntityListGenericEntity(
    List<AbstractEntity> entity, Type genericType) {
    super(entity, genericType);
  }

  /**
   * @param entity
   */
  public AbstractEntityListGenericEntity(
    List<AbstractEntity> entity) {
    super(entity);
  }
}

Explanations

This was one of the solutions I came up with, and the one I preferred. However, I have also tried or though about other ways. The following topics summarize those options:

  • T[] instead of List – to make that work, I would need specific implementations to provide methods that would convert from List to T[] because that cannot be easily implemented in Java; list.toArray(T[]) method needs us to instantiate a new array of T and because T is generic, it cannot be instantiated. Additionally, I would have some performance issues because, everytime I had a list of objects fetched from the database, I would need some time to create a new array from that list. Therefore, I did not even try to implement this option
  • Override the findRange method – curiously (or not), I could have simply overridden the method findRange in AbstractEntityRestController to return a GenericEntity<List<AbstractEntity>> and that would be enough to make it work. However, although it was a simple implementation, it didn’t feel like it was a good implementation. I don’t know exactly how to explain, but it seemed like a hack. Therefore, I have also declined this option

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s