Infinite recursion and Jackson JSON and Hibernate JPA problems

Keywords: Hibernate JSON Java xml

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:

#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.

Posted by Xzone5 on Fri, 14 Feb 2020 06:14:15 -0800