JPA and JSON-B are great solutions to have a vendor independent code and write very quickly a persistence layer serves through HTTP (through Servlet or JAX-RS layer). However, JPA tends to create object cycles in its mapping and therefore, without careness, JSON-B will just fail with an infinite loop (stack overflow exception).

To see how to solve that, let's take an example:

@Data
@Entity
public class Root {
    @Id
    private String id;
    private String name;

    @OneToMany(fetch = FetchType.EAGER, mappedBy = "root")
    private List<Link> links;
}
@Entity
public class Link {
    @Id
    private String id;
    private String name;

    @ManyToOne
    private Root root;
}

These two entities define a graph: root has a list of links but - and this is the common issue between JPA and any (de)serializer - the link has a reference to the root instance.

So if you want to serialize it you should skip the root in link instances (since it is the same than the enclosing one anyway).

There are multiple ways to do that:

  • set it to null - if you pick this one, ensure to not persist the null which would break the relationship,
  • add @JsonbTransient on the nested relationship,
  • don't serialize the entity but a DTO with the model you want (for example the nested Root instance becomes a String hosting the id of the enclosing instance).

This last option is very tempting and generally works but then, if you serialize a link alone (assume you have a RESTful API, it can do PUT /api/link/linkId), then you look the relationship and will need to add it as a query parameter or so to compensate it, not that RESTful or friendly.

So at the end, the nicest solution is to replace the nested entities by another representation which will often be the identifier only - but can be a light representation in some case with the human name but not the full description for example.

This solution must also be done with another constraint: do it only when needed. Concretely, if I serialize a Link as root entity, I keep nested Root entity in the model and if not I will replace it by an identifier. This means it is not the type which is serialized differently but the property.

To be very concrete, here is the serialization we want of a root entity (assuming there are two links):

{
  "id":"root-id",
  "links":["link-id-1","link-id-2"],
  "name":"root"
}

And here is the serialization of the link directly:

{
  "id":"link-id-id",
  "name":"link-1",
  "root": {
    "id":"root-id",
    "links":["link-id-1","link-id-2"],
    "name":"root"
  }
}

Way more consistent for an API, right?

The solution to do that is to implement either custom JSON-B serializers/deserializers or adapters. The first ones are the most powerful but the lowest level and last one are very easy to implement but are both ways (serialization and deserialization) - which is fine here since we want both sides to work. So for this case we will pick the adapters.

In Root entity, we will specify the adapter we'll write on the list of links to ensure it got transformed to the list of id:

@JsonbTypeAdapter(LinkListAdapter.class)
@OneToMany(fetch = FetchType.EAGER, mappedBy = "root")
private List<Link> links;

All the "magic" will happen in LinkListAdapter which will be responsible to:

  • serialize the List<Link> to a List<String>,
  • deserialize the List<String> (identifiers) to the List<Link> (for the runtime)

Tip: there are a ton of strategies there, several can combine a threadlocal/request scoped bean which will pass some request context like query parameters to the adapter to customize the deserialization behavior to speed up things. We will not cover it in this post but note it is possible to do way more.

So overall, our adapter will look like:

public class LinkListAdapter implements JsonbAdapter<List<Link>, List<String>> {
    @Override
    public List<String> adaptToJson(final List<Link> list) {
        return /*todo*/;
    }

    @Override
    public List<Link> adaptFromJson(final List<String> list) {
        return /*todo*/
    }
}

Converting a list of entity to a list of identifier is trivial and can be done with this code:

return list.stream().map(Link::getId).collect(toList());

The opposite is harder since you need to select all entities with an id in the list. Using an EntityManager, it looks like:

final EntityManager em = getEntityManager();
final CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();
final CriteriaQuery<Link> query = criteriaBuilder.createQuery(Link.class);
final Root<Link> root = query.from(Link.class);
final CriteriaQuery<Link> fullQuery = query.where(root.get("id").in(list));
return em.createQuery(fullQuery).getResultList();

But how to get an entity manager? Here again you have multiple options but overall it is about having access to the current entity manager of the application. With CDI/Spring it will often look like a simple injection - scopes doing the work:

public class LinkListAdapter implements JsonbAdapter<List<Link>, List<String>> {
    @Inject // @PersistenceContext etc...depending your app
    @Unit(name="myapp") // using meecrowave-jpa for example
    private EntityManager em;

    @Override
    public List<String> adaptToJson(final List<Link> list) {
        return list.stream().map(Link::getId).collect(toList());
    }

    @Override
    public List<Link> adaptFromJson(final List<String> list) {
        final CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();
        final CriteriaQuery<Link> query = criteriaBuilder.createQuery(Link.class);
        final Root<Link> root = query.from(Link.class);
        final CriteriaQuery<Link> fullQuery = query.where(root.get("id").in(list));
        return em.createQuery(fullQuery).getResultList();
    }
}

Important: a more production friendly implementation will also check all identifiers where loaded or will fail with an optimistic lock exception or so to avoid inconsistencies - but this is not related to JSON-B/JPA link, more to the functional of your endpoints.

If we summarize, we did these steps:

  • Write an adapter to convert a list of entities to their identifiers (and the opposite, load a list of entities from their identifiers),
  • Set this adapter on the relationship field of the entity.

With these two steps, we can use JPA entities from the backend to the frontend of your application very smoothly. Last note is that you will often also implement unary adapter (not always for list) but the logic is really the same, just the deserialization is simpler since it is a plain em.find.

Final note is that this solution has a small pitfall: it breaks all the tooling around your endpoints like Swagger/OpenAPI generators will see the entities so you will need to give them another model or map the model manually to align on the actual implementation.

The choice between this solution and having a real DTO layer you map is mainly around:

  • Speed of development - if you do a quick PoC it is more than enough,
  • The ecosystem you use (if you don't use swagger and friends then you don't really care),
  • Maintenance cost and evolutivity of the database model and endpoint contract (if database moves faster than the HTTP layer it is not relevant to push entities there).

So as usual, it is not because you can that you should but this enables some simple microservices or migration microservices (the service you write to glue 2 versions of another application for example) to be done very quickly and simply.

 

From the same author:

In the same category: