Recursión infinita con Jackson JSON e Hibernación JPA issue


Cuando intento convertir un objeto JPA que tiene una asociación bidireccional en JSON, sigo obteniendo

org.codehaus.jackson.map.JsonMappingException: Infinite recursion (StackOverflowError)

Todo lo que encontré es este hilo que básicamente concluye con recomendar evitar asociaciones bidireccionales. ¿Alguien tiene una idea para una solución para este error de primavera?

------ EDITAR 2010-07-24 16:26:22 -------

Codesnippets:

Objeto de negocio 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 ...

Objeto de negocio 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;

Controlador:

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-implementación del pasante 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();
    }
}

Persistencia.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>
Author: Filip Spiridonov, 2010-07-24

18 answers

Usted puede utilizar @JsonIgnore para romper el ciclo.

 214
Author: axtavt,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-07-01 09:25:44

JsonIgnoreProperties [Actualización de 2017]:

Ahora puede usar JsonIgnoreProperties para suprimir la serialización de propiedades (durante la serialización), o ignorar el procesamiento de propiedades JSON leídas (durante la deserialización). Si esto no es lo que estás buscando, sigue leyendo a continuación.

(Gracias a As Zammel AlaaEddine por señalar esto).


JsonManagedReference y JsonBackReference

Desde Jackson 1.6 puedes usar dos anotaciones para resolver el problema de recursión infinita sin ignorar los getters / setters durante la serialización: @JsonManagedReference y @JsonBackReference.

Explicación

Para que Jackson funcione bien, uno de los dos lados de la relación no debe ser serializado, para evitar el bucle infite que causa el error de stackoverflow.

Entonces, Jackson toma la parte delantera de la referencia (su Set<BodyStat> bodyStats en clase de aprendiz), y la convierte en un almacenamiento similar a json format; este es el llamado proceso marshalling. Luego, Jackson busca la parte posterior de la referencia (es decir, Trainee trainee en la clase BodyStat) y la deja como está, no serializándola. Esta parte de la relación se reconstruirá durante la deserialización (unmarshalling) de la referencia forward.

Puedes cambiar tu código de esta manera (omito las partes inútiles):

Objeto de Negocio 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;

Objeto De Negocio 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;

Ahora todo debería funcionar correctamente.

Si quieres más información, escribí un artículo sobre Json y Jackson Stackoverflow problemas en Keenformatics, mi blog.

EDITAR:

Otra anotación útil que podría comprobar es @JsonIdentityInfo: al usarla, cada vez que Jackson serialice su objeto, agregará un ID (u otro atributo de su elección) a él, de modo que no lo "escaneará" completamente de nuevo cada vez. Este puede ser útil cuando tienes un bucle de cadena entre objetos más interrelacionados (por ejemplo: Order -> OrderLine -> User -> Order y otra vez).

En este caso hay que tener cuidado, ya que podría necesitar leer los atributos de su objeto más de una vez (por ejemplo, en una lista de productos con más productos que comparten el mismo vendedor), y esta anotación le impide hacerlo. Sugiero que siempre echar un vistazo a los registros de firebug para comprobar la respuesta Json y ver lo que está pasando en su codificar.

Fuentes:

 467
Author: Kurt Bourbaki,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-01-24 21:12:13

La nueva anotación @JsonIgnoreProperties resuelve muchos de los problemas con las otras opciones.

@Entity

public class Material{
   ...    
   @JsonIgnoreProperties("costMaterials")
   private List<Supplier> costSuppliers = new ArrayList<>();
   ...
}

@Entity
public class Supplier{
   ...
   @JsonIgnoreProperties("costSuppliers")
   private List<Material> costMaterials = new ArrayList<>();
   ....
}

Échale un vistazo aquí. Funciona igual que en el documentation:
http://springquay.blogspot.com/2016/01/new-approach-to-solve-json-recursive.html

 71
Author: tero17,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-11-03 20:36:18

También, usando Jackson 2.0+ puedes usar @JsonIdentityInfo. Esto funcionó mucho mejor para mis clases de hibernación que @JsonBackReference y @JsonManagedReference, que tenían problemas para mí y no resolvían el problema. Simplemente agregue algo como:

@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 {

Y debería funcionar.

 41
Author: Marcus,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-06-02 18:56:49

Además, Jackson 1.6 tiene soporte para manejar referencias bidireccionales... lo que parece lo que estás buscando (esta entrada de blog también menciona la característica)

Y a partir de julio de 2011, también hay "jackson-module-hibernate" que podría ayudar en algunos aspectos de tratar con objetos Hibernados, aunque no necesariamente este en particular (que requiere anotaciones).

 19
Author: StaxMan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-07-30 17:41:46

Ahora Jackson soporta evitar ciclos sin ignorar los campos:

Jackson-serialización de entidades con relaciones bidireccionales (evitando ciclos)

 11
Author: Eugene Retunsky,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 11:47:28

Esto funcionó perfectamente bien para mí. Agregue la anotación @JsonIgnore en la clase secundaria donde mencione la referencia a la clase principal.

@ManyToOne
@JoinColumn(name = "ID", nullable = false, updatable = false)
@JsonIgnore
private Member member;
 8
Author: Manjunath BR,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-06-09 21:02:26

Ahora hay un módulo Jackson (para Jackson 2) diseñado específicamente para manejar problemas de inicialización perezosa de Hibernación al serializar.

Https://github.com/FasterXML/jackson-datatype-hibernate

Simplemente agregue la dependencia (tenga en cuenta que hay diferentes dependencias para Hibernar 3 e Hibernar 4):

<dependency>
  <groupId>com.fasterxml.jackson.datatype</groupId>
  <artifactId>jackson-datatype-hibernate4</artifactId>
  <version>2.4.0</version>
</dependency>

Y luego registrar el módulo al inicializar el ObjectMapper de Jackson:

ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new Hibernate4Module());

La documentación actualmente no es buena. Ver el Código Hibernate4Module para las opciones disponibles.

 6
Author: Shane,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-12-14 04:50:52

En mi caso fue suficiente cambiar la relación de:

@OneToMany(mappedBy = "county")
private List<Town> towns;

A:

@OneToMany
private List<Town> towns;

Otra relación se quedó como estaba:

@ManyToOne
@JoinColumn(name = "county_id")
private County county;
 4
Author: Klapsa2503,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-08-29 17:31:20

Para mí la mejor solución es usar @JsonView y crear filtros específicos para cada escenario. También puede usar @JsonManagedReference y @JsonBackReference, sin embargo, es una solución codificada para una sola situación, donde el propietario siempre hace referencia al lado propietario, y nunca al contrario. Si tiene otro escenario de serialización en el que necesita volver a anotar el atributo de manera diferente, no podrá hacerlo.

Problema

Vamos a usar dos clases, Company y Employee donde tienes una dependencia cíclica entre ellos:

public class Company {

    private Employee employee;

    public Company(Employee employee) {
        this.employee = employee;
    }

    public Employee getEmployee() {
        return employee;
    }
}

public class Employee {

    private Company company;

    public Company getCompany() {
        return company;
    }

    public void setCompany(Company company) {
        this.company = company;
    }
}

Y la clase de prueba que intenta serializar usando ObjectMapper (Bota de Primavera):

@SpringBootTest
@RunWith(SpringRunner.class)
@Transactional
public class CompanyTest {

    @Autowired
    public ObjectMapper mapper;

    @Test
    public void shouldSaveCompany() throws JsonProcessingException {
        Employee employee = new Employee();
        Company company = new Company(employee);
        employee.setCompany(company);

        String jsonCompany = mapper.writeValueAsString(company);
        System.out.println(jsonCompany);
        assertTrue(true);
    }
}

Si ejecuta este código, obtendrá:

org.codehaus.jackson.map.JsonMappingException: Infinite recursion (StackOverflowError)

Solución Usando '@JsonView '

@JsonView le permite usar filtros y elegir qué campos deben incluirse mientras se serializan los objetos. Un filtro es solo una referencia de clase utilizada como identificador. Así que primero vamos a crear los filtros:

public class Filter {

    public static interface EmployeeData {};

    public static interface CompanyData extends EmployeeData {};

} 

Recuerde, los filtros son clases ficticias, solo se usa para especificar los campos con la anotación @JsonView, para que pueda crear tantos como desee y necesite. Vamos a verlo en acción, pero primero necesitamos anotar nuestra clase Company:

public class Company {

    @JsonView(Filter.CompanyData.class)
    private Employee employee;

    public Company(Employee employee) {
        this.employee = employee;
    }

    public Employee getEmployee() {
        return employee;
    }
}

Y cambie la Prueba para que el serializador use la vista:

@SpringBootTest
@RunWith(SpringRunner.class)
@Transactional
public class CompanyTest {

    @Autowired
    public ObjectMapper mapper;

    @Test
    public void shouldSaveCompany() throws JsonProcessingException {
        Employee employee = new Employee();
        Company company = new Company(employee);
        employee.setCompany(company);

        ObjectWriter writter = mapper.writerWithView(Filter.CompanyData.class);
        String jsonCompany = writter.writeValueAsString(company);

        System.out.println(jsonCompany);
        assertTrue(true);
    }
}

Ahora, si ejecuta este código, el problema de Recursión Infinita está resuelto, porque ha dicho explícitamente que solo desea serializar los atributos que se anotaron con @JsonView(Filter.CompanyData.class).

, Cuando llega a la back reference for company en el Employee, comprueba que no está anotado e ignora la serialización. También tiene una solución potente y flexible para elegir qué datos desea enviar a través de sus API REST.

Con Spring puede anotar sus métodos de Controladores REST con el filtro @JsonView deseado y la serialización se aplica de forma transparente al objeto que regresa.

Aquí están las importaciones utilizadas en caso de que necesite verificar:

import static org.junit.Assert.assertTrue;

import javax.transaction.Transactional;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;

import com.fasterxml.jackson.annotation.JsonView;
 4
Author: fabioresner,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-10-19 17:41:36

Asegúrese de usar com.fasterxml.jackson en todas partes. Pasé mucho tiempo para averiguarlo.

<properties>
  <fasterxml.jackson.version>2.9.2</fasterxml.jackson.version>
</properties>

<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-annotations -->
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-annotations</artifactId>
    <version>${fasterxml.jackson.version}</version>
</dependency>

<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>${fasterxml.jackson.version}</version>
</dependency>

Luego use @JsonManagedReference y @JsonBackReference.

Finalmente, puedes serializar tu modelo a JSON:

import com.fasterxml.jackson.databind.ObjectMapper;

ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(model);
 3
Author: Sveteek,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-10-19 17:36:40

Puede usar el patrón DTO cree la clase TraineeDTO sin ninguna anotación hiberbnate y puede usar jackson mapper para convertir a Trainee a TraineeDTO y bingo el mensaje de error disapeare:)

 1
Author: Dahar Youssef,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-08-20 15:00:47

También me encontré con el mismo problema. Usé el tipo de generador @JsonIdentityInfo's ObjectIdGenerators.PropertyGenerator.class.

Esa es mi solución:

@Entity
@Table(name = "ta_trainee", uniqueConstraints = {@UniqueConstraint(columnNames = {"id"})})
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class Trainee extends BusinessObject {
...
 1
Author: Arif Acar,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-03-14 14:39:50

Puede usar @JsonIgnore, pero esto ignorará los datos json a los que se puede acceder debido a la relación de Clave Foránea. Por lo tanto, si solicita los datos de la clave externa (la mayoría de las veces lo requerimos), entonces @JsonIgnore no lo ayudará. En tal situación, siga la siguiente solución.

Estás obteniendo recursión infinita, debido a que la clase BodyStat nuevamente se refiere al objeto Trainee

BodyStat

@ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinColumn(name="trainee_fk")
private Trainee trainee;

Aprendiz

@OneToMany(mappedBy = "trainee", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@Column(nullable = true)
private Set<BodyStat> bodyStats;

Por lo tanto, usted tiene que comentar / omitir la parte anterior en Aprendiz

 1
Author: RAJ KASHWAN,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-03-27 14:28:59

@JsonIgnoreProperties es la respuesta.

Use algo como esto::

@OneToMany(mappedBy = "course",fetch=FetchType.EAGER)
@JsonIgnoreProperties("course")
private Set<Student> students;
 1
Author: SumanP,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-05-02 00:45:38

Tuve este problema, pero no quería usar anotación en mis entidades, así que resolví creando un constructor para mi clase, este constructor no debe tener una referencia a las entidades que hacen referencia a esta entidad. Digamos que este escenario.

public class A{
   private int id;
   private String code;
   private String name;
   private List<B> bs;
}

public class B{
   private int id;
   private String code;
   private String name;
   private A a;
}

Si intenta enviar a la vista la clase B o A con @ResponseBody puede causar un bucle infinito. Puedes escribir un constructor en tu clase y crear una consulta con tu entityManager así.

"select new A(id, code, name) from A"

Esta es la clase con el constructor.

public class A{
   private int id;
   private String code;
   private String name;
   private List<B> bs;

   public A(){
   }

   public A(int id, String code, String name){
      this.id = id;
      this.code = code;
      this.name = name;
   }

}

Sin embargo, hay algunas constricciones sobre esta solución, como puede ver, en el constructor no hice una referencia a List bs esto es porque Hibernate no lo permite, al menos en versión 3.6.10.Final, así que cuando necesito mostrar ambas entidades en una vista hago lo siguiente.

public A getAById(int id); //THE A id

public List<B> getBsByAId(int idA); //the A id.

El otro problema con esta solución, es que si agrega o elimina una propiedad debe actualizar su constructor y todas sus consultas.

 0
Author: OJVM,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-11-30 21:16:37

En caso de que esté utilizando Spring Data Rest, el problema se puede resolver creando Repositorios para cada Entidad involucrada en referencias cíclicas.

 0
Author: Morik,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-04-20 20:54:28

Trabajando bien para mí Resolver el problema de Recursión infinita Json cuando se trabaja con Jackson

Esto es lo que he hecho en OneToMany y ManyToOne Mapping

@ManyToOne
@JoinColumn(name="Key")
@JsonBackReference
private LgcyIsp Key;


@OneToMany(mappedBy="LgcyIsp ")
@JsonManagedReference
private List<Safety> safety;
 0
Author: Prabu M,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-05-28 05:19:56