Search code examples
jsonspringhibernaterecursionprojection

Spring Data REST Projection with bidirectional relationship resulting JSON infinite recursion


I'm working in a project using Spring Boot 2.0, Hibernate and Spring Data REST. FrontEnd with React.
I've the situation where a User can be linked with several Companies (he owns more than one Company).
When I try to get some of the entities, using the UserRepository or CompanyRepository, I get the error: Could not write JSON: Infinite recursion (StackOverflowError); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError).

I have to use Projections to limit the data going to the FrontEnd and because I need the links to the entities, auto generated by Projections.

Follow the entities:

@Entity
public class User implements UserDetails {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id_user")
    protected Long id;

    @OneToMany(cascade= { CascadeType.MERGE }, fetch = FetchType.EAGER, mappedBy="user")
    private List<Company> companyList;

    // Other data
    // Getters and Setters
}

@Entity
public class Company extends CadastroEmpresaUnica {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id_company")
    protected Long id;

    @ManyToOne(cascade= { CascadeType.MERGE })
    @JoinColumn(name="id_user", nullable = false)
    private User user;

    // Other data
    // Getters and Setters
}

The Projections:

@Projection(name = "userProjection", types = { User.class })
public interface UserProjection {

    List<CompanyProjection> getCompanyList();

    // Other Getters
}

@Projection(name = "companyProjection", types = { Company.class }) 
public interface CompanyProjection {
    UserProjection getUser();

    // Other Getters
}

One of the Repositories we are using:

@RepositoryRestResource(collectionResourceRel = "company", path = "companies", excerptProjection = CompanyProjection.class)
public interface CompanyRepository extends PagingAndSortingRepository<Company, Long>, CompanyRepositoryCustom, JpaSpecificationExecutor<Company> {}

Searching about bidirectional infinite recursion I found content about '@JsonManagedReference' and '@JsonBackReference', always been used directly in the Entities. So I tried to use in my Projections and it worked. So it resolve my problem of infinite recursion, but it generates another problem, I cant access my User from my Company (because apparently '@JsonBackReference' don't get it to stop the recursion).
Here is my Projections with this solution:

@Projection(name = "userProjection", types = { User.class })
public interface UserProjection {
    @JsonManagedReference
    List<CompanyProjection> getCompanyList();

    // Other Getters
}

@Projection(name = "companyProjection", types = { Company.class }) 
public interface CompanyProjection {
    @JsonBackReference
    UserProjection getUser();

    // Other Getters
}

Searching a little more I read about '@JsonIdentityInfo', again, been used in the Entity. So I tried to remove the other Json annotations and use '@JsonIdentityInfo' in my Projection. As the following examples:

@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property="id")
@Projection(name = "userProjection", types = { User.class })
public interface UserProjection {
    Long getId();
    List<CompanyProjection> getCompanyList();

    // Other Getters
}

@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property="id")
@Projection(name = "companyProjection", types = { Company.class }) 
public interface CompanyProjection {
    Long getId();
    UserProjection getUser();

    // Other Getters
}

It didn't work. Now the Json infinite recursion is happening again.
I am new with Spring Data REST and I am really trying to understand better Projections with Spring Data Rest, reading the Spring documentation and Stackoverflow topics. I would like to know what I'm doing wrong and, of course, if I'm using Projections the wrong way, but I need to go ahead with this project.


Solution

  • The best way we find would be with the Jackson annotation @JsonIgnoreProperties, that should be used in parent list to igore himself in the child. But, after a few tries, seems like this annotation is not working in projections, specifically for Spring Data REST.
    Follow the example of what would be the correct way:

    @Projection(name = "userProjection", types = { User.class })
    public interface UserProjection {
        @JsonIgnoreProperties({"user"})
        List<CompanyProjection> getCompanyList();
    
        // Other Getters
    }
    
    @Projection(name = "companyProjection", types = { Company.class }) 
    public interface CompanyProjection {
        UserProjection getUser();
    
        // Other Getters
    }
    

    We send a ticket for this Spring Data REST issue and it has been accepted. We believe in a near future it will be corrected and we can use it.
    For now, we adjust our projections so the list object can use a "derivation" of the original projection, ignoring the property that causes the infinite recursion.
    Follow the example:

    @Projection(name = "userProjection", types = { User.class })
    public interface UserProjection {
        List<CompanyProjectionWithoutUser> getCompanyList();
    
        // Other Getters
    
        // Projection without the User, that couses infinite recursion
        public interface CompanyProjectionWithoutUser extends CompanyProjection {
            @Override
            @JsonIgnore
            UserProjection getUser();
        }
    }
    
    @Projection(name = "companyProjection", types = { Company.class }) 
    public interface CompanyProjection {
        UserProjection getUser();
    
        // Other Getters
    }