Filtering lazy-loaded JPA fields in JAX-RS with Jackson

29th
Jul
2020

While working on a side project using a JAX-RS API and JPA I wanted to explore how the entity graph features introduced in JPA 2.1 could be used to simplify data fetches for different endpoints. One issue I quickly ran into was how to deal with LazyInitializationExceptions with unloaded lazy fields.

The Hibernate module supplied by Jackson didn’t meet my needs and I couldn’t find any examples of what I needed so it was time to delve into how Jackson filtering works. The end solution turned out to be fairly straightforward and doesn’t rely on any vendor specifics so it’s worth sharing.

The original project was written in Scala but I have converted the code to Java for this example. The project and example have both been written for Wildfly 20 and Jackson 2.11 but should work on any Java/Jakarta EE 8+ server.

Initial model and resource

We are going to start with a very simple Article entity:

Article.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Entity
@Table(name = "article")
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@JsonProperty
private Long id;

@Column(name = "title", nullable = false)
@NotNull
@JsonProperty
private String title;

@OneToMany(cascade = CascadeType.ALL, mappedBy = "article", fetch = FetchType.EAGER)
@Column(name = "tag")
@JsonProperty
private List<Tags> tags;
}

That has a collection of Tags:

Tag.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Entity
@Table(name = "tag")
public class Tags {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@JsonProperty
private Long id;

@ManyToOne(optional = false)
@JoinColumn(name = "article", updatable = false)
@JsonIgnore
private Article article;

@Column(name = "tag")
@JsonProperty
private String tag;
}

Fetching and returning Articles is handled by a simple JAX-RS resource:

ArticleResource.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Path("/articles")
@Produces(MediaType.APPLICATION_JSON)
public class ArticleResource {
@PersistenceContext
private EntityManager entityManager;

@GET
@Path("/{id:[\\d]+}")
public Response show(@PathParam("id") Long id,) {
Article article = findById(id);
return Response.ok(new ExampleWrapper(article)).build();
}

private Article findById(Long id) {
return entityManager.find(Article.class, id);
}

public static class ExampleWrapper {
@JsonProperty
private final Article article;

public ExampleWrapper(Article article) {
this.article = article;
}
}
}

Calling this endpoint returns a single article with an embedded list of tags:

GET /articles/1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"article": {
"id": 1,
"title": "Coding with JAX-RS and JPA for fun and profit",
"tags": [
{
"id": 1,
"tag": "jax-rs"
},
{
"id": 2,
"tag": "jpa"
}
]
}
}

Other boilerplate such as configuring JAX-RS, JPA, and Jackson are omitted - we are using the defaults for these.

Making tags lazy

Now we want to implement a requirement to only fetch tags if the API request explicitly requests them with an include_tags query parameter[1]. The entity graph support in JPA 2.1 is an ideal fit for this feature as allows switching the fetch behaviour without needing to change the query.

We start by adding a NamedEntityGraph to the Article entity and changing the fetch mode of tags back to the default of LAZY:

Article.java
1
2
3
4
5
6
7
@NamedEntityGraph(name = "tags", attributeNodes = @NamedAttributeNode("tags"))
public class Article {
@OneToMany(cascade = CascadeType.ALL, mappedBy = "article", fetch = FetchType.LAZY)
@Column(name = "tag")
@JsonProperty
private List<Tags> tags;
}

The new query parameter can be added to the JAX-RS endpoint and the find call extended to use the new entity graph:

ArticleResource.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ArticleResource {
@GET
@Path("/{id:[\\d]+}")
public Response show(@PathParam("id") Long id, @QueryParam("include_tags") boolean includeTags) {
Article article = findById(id, includeTags);
return Response.ok(new ExampleWrapper(article)).build();
}

private Article findById(Long id, boolean includeTags) {
Map<String, Object> props = new HashMap<>();

if (includeTags) {
EntityGraph<?> graph = entityManager.getEntityGraph("tags");
props.put("javax.persistence.fetchgraph", graph);
}

return entityManager.find(Article.class, id, props);
}
}

Calling the endpoint with the new query parameter set (include_tags=true) works as expected and returns the same JSON as before. But if we remove the query parameter we get a 500 response and an ugly stack trace from Hibernate as the tags have not been fetched:

1
2
3
4
5
6
7
8
9
10
11
UT005023: Exception handling request to /example/api/articles/1: org.jboss.resteasy.spi.UnhandledException:
com.fasterxml.jackson.databind.JsonMappingException: failed to lazily initialize a collection of role:
com.example.lazy.model.Article.tags, could not initialize proxy - no Session (through reference chain:
com.example.lazy.api.ArticleResource$ExampleWrapper["article"]->com.example.lazy.model.Article["tags"])
at org.jboss.resteasy.core.ExceptionHandler.handleException(ExceptionHandler.java:356)
at org.jboss.resteasy.core.SynchronousDispatcher.writeException(SynchronousDispatcher.java:193)
...
Caused by: org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role:
com.example.lazy.model.Article.tags, could not initialize proxy - no Session
at org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:602)
...

Jackson provides a solution to this with their Hibernate modules [2] which will detect Hibernate proxy objects that are not initialised and handle them for us. If we enable the Hibernate5Module and retry the request we get this response:

GET /articles/1?include_tags=false
1
2
3
4
5
6
7
{
"article": {
"id": 1,
"title": "Coding with JAX-RS and JPA for fun and profit",
"tags": null
}
}

No stack traces and a valid JSON response - great! But the response has "tags": null which implies that there are no tags defined. We know that there are tags by making a request with include_tags=true so having an explicit null value here is misleading - a better solution would be to omit the tags field entirely.

Filtering lazy fields

Jackson includes a PropertyFilter interface that can be used to dynamically exclude properties at runtime. We can set up a filter to detect lazy fields and omit the property if they haven’t been loaded:

LazyLoadPersistenceFilter.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class LazyLoadPersistenceFilter extends SimpleBeanPropertyFilter {
@Override
public void serializeAsField(Object pojo,
JsonGenerator jgen,
SerializerProvider provider,
PropertyWriter writer) throws Exception {

if (include(pojo, writer)) {
writer.serializeAsField(pojo, jgen, provider);
} else {
writer.serializeAsOmittedField(pojo, jgen, provider);
}
}

private boolean include(Object pojo, PropertyWriter writer) throws Exception {
if (writer instanceof BeanPropertyWriter) {
BeanPropertyWriter beanWriter = (BeanPropertyWriter) writer;

Object fieldValue = beanWriter.get(pojo);
return Persistence.getPersistenceUtil().isLoaded(fieldValue);
}

return true;
}
}

The PersistenceUtil.isLoaded is a new feature added in JPA 2.0 that returns true if an entity has been fetched or is not subject to lazy loading.

This new filter gets wired into the ObjectMapper:

ObjectMapperResolver.java
1
2
3
4
5
6
7
8
9
10
@Provider
public class ObjectMapperResolver implements ContextResolver<ObjectMapper> {
@Override
public ObjectMapper getContext(Class<?> type) {
FilterProvider filterProvider = new SimpleFilterProvider()
.addFilter("lazy-load", new HibernateProxyFilter());

return new ObjectMapper().setFilterProvider(filterProvider);
}
}

And finally added to the Article entity[3]:

Article.java
1
2
3
4
@JsonFilter("lazy-load")
public class Article {
// ...
}

Now when we make a lazy request we get a response with the tags field omitted:

GET /articles/1?include_tags=false
1
2
3
4
5
6
{
"article": {
"id": 1,
"title": "Coding with JAX-RS and JPA for fun and profit"
}
}

Conclusions

The Jackson Hibernate modules are a great fit for many situations but they don’t handle the scenario where you want to completely omit lazy fields. Thankfully it is easy to implement a PropertyFilter that uses PersistenceUtil to exclude unloaded lazy fields.

JPA 2.1 entity graphs are fantastic for handling lazy-loading across several JAX-RS endpoints. A set of entity graphs can be easily combined and used by multiple queries, dramatically simplifying fetch code. The only gripe so far is the way that entity graphs are selected using query hints - I feel that extending the API for Query (and TypedQuery) would have been a cleaner solution.

Full example code for this post can be found on GitHub.


  1. For a real-world application this would more likely be including tags on a show endpoint and excluding them on the list endpoint. ↩︎

  2. Hibernate3Module, Hibernate4Module and Hibernate5Module depending your Hibernate version. ↩︎

  3. This filter could be also added to a base entity class or applied globally using a Jackson mixin class. ↩︎