When trying to convert a JPA object with two-way associations to JSON, I kept
org.codehaus.jackson.map.JsonMappingException: Infinite recursion (StackOverflowError)
All I found was The thread , basically ending with a recommendation to avoid two-way Association. Who knows the wrong solution this spring?
------Edited on July 24, 2010 16:26:22-------
Code segment:
Business object 1:
@Entity @Table(name = "ta_trainee", uniqueConstraints = {@UniqueConstraint(columnNames = {"id"})}) public class Trainee extends BusinessObject { @Id @GeneratedValue(strategy = GenerationType.TABLE) @Column(name = "id", nullable = false) private Integer id; @Column(name = "name", nullable = true) private String name; @Column(name = "surname", nullable = true) private String surname; @OneToMany(mappedBy = "trainee", fetch = FetchType.EAGER, cascade = CascadeType.ALL) @Column(nullable = true) private Set<BodyStat> bodyStats; @OneToMany(mappedBy = "trainee", fetch = FetchType.EAGER, cascade = CascadeType.ALL) @Column(nullable = true) private Set<Training> trainings; @OneToMany(mappedBy = "trainee", fetch = FetchType.EAGER, cascade = CascadeType.ALL) @Column(nullable = true) private Set<ExerciseType> exerciseTypes; public Trainee() { super(); } ... getters/setters ...
Business object 2:
import javax.persistence.*; import java.util.Date; @Entity @Table(name = "ta_bodystat", uniqueConstraints = {@UniqueConstraint(columnNames = {"id"})}) public class BodyStat extends BusinessObject { @Id @GeneratedValue(strategy = GenerationType.TABLE) @Column(name = "id", nullable = false) private Integer id; @Column(name = "height", nullable = true) private Float height; @Column(name = "measuretime", nullable = false) @Temporal(TemporalType.TIMESTAMP) private Date measureTime; @ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL) @JoinColumn(name="trainee_fk") private Trainee trainee;
Controller:
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import javax.servlet.http.HttpServletResponse; import javax.validation.ConstraintViolation; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @Controller @RequestMapping(value = "/trainees") public class TraineesController { final Logger logger = LoggerFactory.getLogger(TraineesController.class); private Map<Long, Trainee> trainees = new ConcurrentHashMap<Long, Trainee>(); @Autowired private ITraineeDAO traineeDAO; /** * Return json repres. of all trainees */ @RequestMapping(value = "/getAllTrainees", method = RequestMethod.GET) @ResponseBody public Collection getAllTrainees() { Collection allTrainees = this.traineeDAO.getAll(); this.logger.debug("A total of " + allTrainees.size() + " trainees was read from db"); return allTrainees; } }
JPA implementation student DAO:
@Repository @Transactional public class TraineeDAO implements ITraineeDAO { @PersistenceContext private EntityManager em; @Transactional public Trainee save(Trainee trainee) { em.persist(trainee); return trainee; } @Transactional(readOnly = true) public Collection getAll() { return (Collection) em.createQuery("SELECT t FROM Trainee t").getResultList(); } }
persistence.xml
<persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0"> <persistence-unit name="RDBMS" transaction-type="RESOURCE_LOCAL"> <exclude-unlisted-classes>false</exclude-unlisted-classes> <properties> <property name="hibernate.hbm2ddl.auto" value="validate"/> <property name="hibernate.archive.autodetection" value="class"/> <property name="dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect"/> <!-- <property name="dialect" value="org.hibernate.dialect.HSQLDialect"/> --> </properties> </persistence-unit> </persistence>
#1 building
Now, Jackson supports avoiding loops without ignoring fields:
Jackson - serialization of entities with two-way relationships (avoid loops)
#2 building
Jsonignoreproperties [update 2017]:
Now, you can use the JsonIgnoreProperties Suppress serialization of properties (during serialization), or ignore processing of JSON property reads (during deserialization). If this is not what you want, please read on.
(thanks to as zammel Alaa eddine for pointing this out.).
JsonManagedReference and JsonBackReference
Starting with Jackson 1.6, you can use two annotations to solve infinite recursion without omitting getter / setter during serialization: @JsonManagedReference and @JsonBackReference .
Explain
For Jackson to work properly, one of the two aspects of the relationship should not be serialized to avoid the infite loop that causes your stack overflow error.
Therefore, Jackson accepts the previous part of the reference (set < BodyStat > bodystats in the Trainee class) and converts it to a json like storage format; this is the so-called marshalling process. Jackson then looks for the second half of the reference (that is, the Trainee trainee in the BodyStat class) and keeps it as it is, without serializing it. This part of the relationship is rebuilt during deserialization (unmarshalling) of the forward reference.
You can change the code like this (I skipped the useless part):
Business object 1:
@Entity @Table(name = "ta_trainee", uniqueConstraints = {@UniqueConstraint(columnNames = {"id"})}) public class Trainee extends BusinessObject { @OneToMany(mappedBy = "trainee", fetch = FetchType.EAGER, cascade = CascadeType.ALL) @Column(nullable = true) @JsonManagedReference private Set<BodyStat> bodyStats;
Business object 2:
@Entity @Table(name = "ta_bodystat", uniqueConstraints = {@UniqueConstraint(columnNames = {"id"})}) public class BodyStat extends BusinessObject { @ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL) @JoinColumn(name="trainee_fk") @JsonBackReference private Trainee trainee;
Now everything should work.
If you want more information, I stay My blog On Keenformatics Wrote an article about On the problems of Jason and Jackson Stackoverflow Article.
Editor:
Another useful comment you can check is @JsonIdentityInfo : with it, every time Jackson serializes your object, it adds an ID (or another property of your choice) to it, so it doesn't "scan" it completely every time. This is useful when you form chain loops between more related objects (for example: order - > Orderline - > User - > order and over).
In this case, you must be careful because you may need to read the properties of the object multiple times (for example, there are multiple products sharing the same vendor in a product list), and this comment prevents you from doing so. I recommend always checking the Firebug log to check the Json response and see what happens in the code.
Source:
- Keenformatics - how to solve JSON infinite recursive stack overflow (my blog)
- Jackson reference
- personal experience
#3 building
In addition, with Jackson 2.0 +, you can use @ JsonIdentityInfo. For my dormancy class, this is better than @ JsonBackReference and @ JsonManagedReference. This is problematic for me, but it can't solve the problem. Just add the following:
@Entity @Table(name = "ta_trainee", uniqueConstraints = {@UniqueConstraint(columnNames = {"id"})}) @JsonIdentityInfo(generator=ObjectIdGenerators.IntSequenceGenerator.class, property="@traineeId") public class Trainee extends BusinessObject { @Entity @Table(name = "ta_bodystat", uniqueConstraints = {@UniqueConstraint(columnNames = {"id"})}) @JsonIdentityInfo(generator=ObjectIdGenerators.IntSequenceGenerator.class, property="@bodyStatId") public class BodyStat extends BusinessObject {
It should work.
#4 building
For me, it's enough to change the relationship from:
@OneToMany(mappedBy = "county") private List<Town> towns;
To:
@OneToMany private List<Town> towns;
Another relationship remains the same:
@ManyToOne @JoinColumn(name = "county_id") private County county;
#5 building
Now there is a Jackson module designed for Jackson 2 to handle Hibernate delay initialization during serialization.
https://github.com/FasterXML/jackson-datatype-hibernate
Just add the dependency (note that Hibernate 3 and Hibernate 4 have different dependencies):
<dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-hibernate4</artifactId> <version>2.4.0</version> </dependency>
Then register the module when initializing Jackson's ObjectMapper:
ObjectMapper mapper = new ObjectMapper(); mapper.registerModule(new Hibernate4Module());
The documentation is not very good at the moment. see also Hibernate4Module code For available options.