Search code examples
javajsonspring-bootspring-data-jpaspring-data

I don't know why @JsonManagedReference & @JsonBackReference & @JsonIgnore don't work on a particular entity, is there any other solution?


I'm working on a Spring Boot application using JPA and Jackson for serializing entities to JSON. I have three models, two with bidirectional relationships, and I'm encountering an infinite recursion issue when trying to serialize one of the objects.

Here are my models:

Competence.java

@Data @AllArgsConstructor @NoArgsConstructor @Entity
public class Competence {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @Column(unique=true) 
    private String nom;
}

CompetenceUtilisateur**.java**

//@JsonIdentityInfo(
//        generator = ObjectIdGenerators.PropertyGenerator.class,
//        property = "id")

@Data @AllArgsConstructor @NoArgsConstructor @Entity
public class CompetenceUtilisateur {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String nom;

    @Enumerated(EnumType.STRING)
    private Niveau niveau;

    private Integer idListDesCompetences;

    //============ Relation =============//

    @ManyToOne
    @JoinColumn(name = "Competence_Id", nullable = false)
    private Competence competence;

    @ManyToOne
//    @JsonBackReference
//    @JsonIgnore
    private ListCompetencesUtilisateur ListDesCompetences;

    //============ Les Methodes =============//

    public void setCompetenceDeProjet(ListCompetencesRequise listcompetenceDeProjet) {
        this.ListDesCompetencesRequise = listcompetenceDeProjet;
        this.idListCompetencesRequise = listcompetenceDeProjet.getId();
    }

    public void setCompetences(Competence competences) {
        this.competences = competences;
        this.nom = competences.getNom();
    }
}

ListCompetencesUtilisateur .java

@Data @AllArgsConstructor @NoArgsConstructor @Entity
public class ListCompetencesUtilisateur {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private Integer idUtilisateur;

    //============ Relation =============//

    @OneToOne @JsonIgnore
    private Utilisateur utilisateur;

    @OneToMany( mappedBy = "ListDesCompetences")
//    @JsonManagedReference
    private List<CompetenceUtilisateur> CompetenceUtilisateur;

    //============ Les Methodes =============//

    public void setcompetenceDeUtilisateur(Utilisateur utilisateur) {
        this.utilisateur = utilisateur;
        this.idUtilisateur = utilisateur.getId();
    }

}

Issue

When I call the REST API to fetch an object of type ListCompetencesUtilisateur, the response JSON results in an infinite recursion because of the bidirectional relationships.

Here's an example:

{
  "id": 1,
  "idUtilisateur": 3,
  "competenceUtilisateur": [
    {
      "id": 1,
      "nom": "Java",
      "listDesCompetences": {
        "id": 1,
        "idUtilisateur": 3,
        "competenceUtilisateur": [
          {
            "id": 1,
            "nom": "Java",
            "listDesCompetences": {
              ...
            }
          }
        ]
      }
    }
  ]
}

Steps I’ve Tried

  1. Added @JsonIgnore on one side of the relationship.

  2. Used @JsonManagedReference and @JsonBackReference annotations.

  3. Tried @JsonIdentityInfo with ObjectIdGenerators.PropertyGenerator.class It works if I use RestController but the problem is that I use @RepositoryRestResource and I get this error: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Invalid Object Id definition for `xxx.xxxxx.appservice.Entity.ListCompetencesUtilisateur`: cannot find property with name 'id' (through reference chain: org.springframework.hateoas.PagedModel["_embedded"]->java.util.LinkedHashMap["listCompetencesUtilisateurs"]->java.util.ArrayList[0])

None of these solutions resolved the issue. I still get infinite recursion when serializing objects.

Is GraphQL a perfect solution for this problem ?

When fetching an object of type ListCompetencesUtilisateur, I want to include the list of CompetenceUtilisateur, but without the nested references that cause an infinite loop.

For example:

{
  "id": 1,
  "idUtilisateur": 3,
  "competenceUtilisateur": [
    {
      "id": 1,
      "nom": "Java",
      "competences": {
        "id": 1,
        "nom": "Java"
      }
    }
  ]
}

Solution

  • To decouple the database layer from the web layer in Spring Boot, I primarily use view models in the web layer. View models are lightweight, easy to implement (often as records), and serve as a dedicated representation of data for the client. This separation ensures that the web layer does not directly expose or depend on the structure of the underlying database entities, promoting better maintainability and flexibility.

    In this case, three view models should suffice. Note the static methods for converting from entity to view model.

    ListCompetencesUtilisateurVm

    public record ListCompetencesUtilisateurVm(
            Integer id,
            Integer idUtilisateur,
            List<CompetenceUtilisateurVm> competenceUtilisateur
    ) {
        public static ListCompetencesUtilisateurVm fromListCompetencesUtilisateur(
                ListCompetencesUtilisateur source) {
    
            return new ListCompetencesUtilisateurVm(
                    source.getId(),
                    source.getIdUtilisateur(),
                    source.getCompetenceUtilisateur().stream()
                            .map(CompetenceUtilisateurVm::fromCompetenceUtilisateur)
                            .toList()
            );
        }
    }
    

    CompetenceUtilisateurVm

    public record CompetenceUtilisateurVm(
            Integer id,
            String nom,
            CompetenceVm competence
    ) {
        public static CompetenceUtilisateurVm fromCompetenceUtilisateur(CompetenceUtilisateur source) {
            return new CompetenceUtilisateurVm(
                    source.getId(), source.getNom(),
                    new CompetenceVm(source.getCompetence().getId(), source.getCompetence().getNom())
            );
        }
    }
    

    CompetenceVm

    public record CompetenceVm(
            Integer id,
            String nom) {
    }