Coding in Spring 3 with JPA and Jackson is supposed to semplify your life, sparing you from writing thousand lines of code. Sometimes, though, you’ll notice that all these annotations hide the real core of your application, preventing you to really understand what your code is doing (and what it is supposed to do).
This means that if you want to develop a good java application or website, you need to carefully read the docs or (more realistically) you need to understand how to read your error logs and combine them with a well-written search engine query.
This is what I had to do when I first met the problem I’m about to write about.
The problem.
I was getting a wrong JSON response while using a many-to-many relationship, that caused my application to enter in an infinite loop giving as output an infinite recursion Stackoverflow error.
Here is part of what I found in my Eclipse console:
Request processing failed; nested exception is org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Infinite recursion (StackOverflowError) (through reference chain: [here is the loop]) with root cause
java.lang.StackOverflowError
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:567)
at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:143)
at com.fasterxml.jackson.databind.ser.std.CollectionSerializer.serializeContents(CollectionSerializer.java:118)
at com.fasterxml.jackson.databind.ser.std.CollectionSerializer.serializeContents(CollectionSerializer.java:24)
at com.fasterxml.jackson.databind.ser.std.AsArraySerializerBase.serialize(AsArraySerializerBase.java:180)
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:544)
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:551)
...
and so on.
Since I was using Datatables jQuery plugin, I also had this output in my browser window:
DataTables warning (table id = 'products'): DataTables warning: JSON data from server could not be parsed. This is caused by a JSON formatting error.
What I noticed is that the same database query, without JSON, was working well. So the problem (as the console in fact said) was with Jackson and Json serialization. As the docs state (see references):
bi-directional references for ORM-managed beans (iBatis, Hibernate) would cause serialization to failure since they are cyclic dependencies.
Since Jackson 1.6, this problem has been solved by the introduction of two new annotations: @JsonManagedReference
and @JsonBackReference
(and see the end of this post to give a look at the @JsonIdentityInfo
annotation).
How does serialization work?
(The solution in theory)
In computer science, in the context of data storage and transmission, serialization is the process of translating data structures or object state into a format that can be stored (for example, in a file or memory buffer, or transmitted across a network connection link) and resurrected later in the same or another computer environment. (Wikipedia)
For Jackson to work well, one of the two sides of the relationship should not be serialized, in order to avoid the annoying infinite recursive loop that causes our stackoverflow error.
So, Jackson takes the forward part of the reference, for example an attribute of a java class (i.e. List<Role> roles
in User
class), and converts it in a json-like storage format; this is the so-called marshalling process.
Then, Jackson looks for the back part of the reference (i.e. List<User> users
in Role
class) and leaves it as it is, not serializing it. This part of the relationship will be re-constructed during the deserialization (unmarshalling) of the forward reference.
The solution in practice.
It’s very simple. Assuming that your database query already works without JSON, all you have to do is this:
- Add the
@JsonManagedReference
In the forward part of the relationship (i.e.User.java
class):@Entity public class User implements java.io.Serializable{ @Id @GeneratedValue(strategy=GenerationType.IDENTITY) private long id; @Column(name="name") private String name; @ManyToMany @JoinTable(name="users_roles",joinColumns=@JoinColumn(name = "user_fk"), inverseJoinColumns=@JoinColumn(name = "role_fk")) @JsonManagedReference private Set<Role> roles = new HashSet<Role>(); ...
- Add the
@JsonBackReference
In the back part of the relationship (i.e.Role.java
class):@Entity public class Role implements java.io.Serializable { @Id @GeneratedValue(strategy=GenerationType.IDENTITY) private int id; @ManyToMany(mappedBy="roles") @JsonBackReference private Set<User> users = new HashSet<User>(); ...
The work is done. If you take a look at your firebug logs, you’ll notice that the infinite recursive loop has disappeared.
Edit (02/09/2013)
Another useful annotation you could check is @JsonIdentityInfo: using it, everytime Jackson serializes your object, it will add an ID (or another attribute of your choose) to it, so that it won’t entirely “scan” it again everytime. This can be useful when you’ve got a chain loop between more interrelated objects (for example: Order -> OrderLine -> User -> Order and over again).
In this case you’ve got to be careful, since you could need to read your object’s attributes more than once (for example in a products list with more products that share the same seller), and this annotation prevents you to do so. I suggest to always take a look at firebug logs to check the Json response and see what’s going on in your code.