Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Eliminate circular JSON in Java Spring Many to Many Relationship

I have a Spring application with 2 Entities that have a Many-to-Many relationship. There are Students and Groups. A student can be part of many groups and a group can have many students.

Student Model

@Entity
@Table(name = "STUDENTS")
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
@JsonIdentityInfo(
        generator = ObjectIdGenerators.PropertyGenerator.class,
        property = "id")

public class Student extends AbstractUser {

    //Fields

    @ManyToMany(fetch = FetchType.LAZY, targetEntity = Group.class)
    @JoinTable(name = "GROUPS_STUDENTS",
            joinColumns = { @JoinColumn(name = "student_id") },
            inverseJoinColumns = { @JoinColumn(name = "group_id") })
    private List<Group> groups = new ArrayList<Group>();

    //Constructors

    public Student(String password, String firstName, String lastName, String email){
        super(password, firstName, lastName, email);
    }

    public Student(){
    }

    //Setters and getters

    public List<Group> getGroups() {
        return groups;
    }

    public void setGroups(List<Group> groups) {
        this.groups = groups;
    }

}

Group Model

@Entity
@Table(name = "GROUPS")
@JsonIdentityInfo(
        generator = ObjectIdGenerators.PropertyGenerator.class,
        property = "id")
public class Group implements Item, Serializable {

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

    @Column(name = "name", nullable = false, unique = true)
    private String name;

    @Column(name = "year", nullable = false, length = 1)
    private int year;

    @ManyToMany(mappedBy = "groups", targetEntity = Student.class)
    private List<Student> students;

    public Group(String name, int yearOfStudy) {
        this.setName(name);
        this.setYear(yearOfStudy);
    }
...
}

The problem is when I make a request to show all students, if 2 students are in the same group they appear in a hierarchy rather than one after another. What I mean the JSON is going too deep. I don't know how to explain exactly but I'll put an example.

How it appears

[
  {
    "id": 2,
    "password": "$2a$10$bxieGA7kWuEYUUMbYNiYo.SbGpo7X5oh8ulUsqCcrIR2cFN2HiQP2",
    "firstName": "First",
    "lastName": "Last",
    "email": "[email protected]",
    "groups": [
      {
        "id": 1,
        "name": "G1",
        "year": 0,
        "users": [
          2,
          {
            "id": 1,
            "password": "$2a$10$9pfTdci7PeHtzxuAxjcOsOjPSswrU35/JOOeWPpgVhJI4tD2YpZdG",
            "firstName": "John",
            "lastName": "Smith",
            "email": "[email protected]",
            "groups": [
              1
            ]
          }
        ]
      }
    ]
  },
  1
]

How it should appear

  {
    "id": 2,
    "password": "$2a$10$bxieGA7kWuEYUUMbYNiYo.SbGpo7X5oh8ulUsqCcrIR2cFN2HiQP2",
    "firstName": "First",
    "lastName": "Last",
    "email": "[email protected]",
    "groups": [
      {
        "id": 1,
        "name": "G1",
        "year": 0,
      }
    ]
  },
  {
    "id": 1,
    "password": "$2a$10$9pfTdci7PeHtzxuAxjcOsOjPSswrU35/JOOeWPpgVhJI4tD2YpZdG",
    "firstName": "John",
    "lastName": "Smith",
    "email": "[email protected]",
    "groups": [
      {
        "id": 1,
        "name": "G1",
        "year": 0
      }]
  }
]

Do you have any idea how to solve this? I don't know exactly how to describe my problem and that's why I didn't find a solution yet. Any help will much appreciated. Thank you.

like image 749
Ionut Robert Avatar asked Dec 31 '16 11:12

Ionut Robert


4 Answers

Using the @JsonIgnoreProperties annotation is another alternative:

@Entity
public class Student extends AbstractUser {
    @ManyToMany(fetch = FetchType.LAZY, targetEntity = Group.class)
    @JoinTable(name = "GROUPS_STUDENTS",
            joinColumns = { @JoinColumn(name = "student_id") },
            inverseJoinColumns = { @JoinColumn(name = "group_id") })
    @JsonIgnoreProperties("students")
    private List<Group> groups = new ArrayList<Group>();
}

@Entity
public class Group implements Item, Serializable {
    @ManyToMany(mappedBy = "groups", targetEntity = Student.class)
    @JsonIgnoreProperties("groups")
    private List<Student> students;
}

Find comparison between @JsonManagedReference+@JsonBackReference, @JsonIdentityInfo and @JsonIgnoreProperties here: http://springquay.blogspot.com/2016/01/new-approach-to-solve-json-recursive.html

like image 78
DurandA Avatar answered Nov 12 '22 11:11

DurandA


I've solved it. I've made a custom serializer. So in Group I'll serialize the students by setting a custom annotation @JsonSerialize(using = CustomStudentSerializer.class)

CustomStudentSerializer

public class CustomStudentSerializer extends StdSerializer<List<Student>> {

    public CustomStudentSerializer() {
        this(null);
    }

    public CustomStudentSerializer(Class<List<Student>> t) {
        super(t);
    }

    @Override
    public void serialize(
            List<Student> students,
            JsonGenerator generator,
            SerializerProvider provider)
            throws IOException, JsonProcessingException {

        List<Student> studs = new ArrayList<>();
        for (Student s : students) {
            s.setGroups(null);
            studs.add(s);
        }
        generator.writeObject(studs);
    }
}

Did the same for the groups. I've just removed the students/group component when the relationship is already nested. And now it works just fine.

Took me a while to figure this out but I posted here because it may help someone else too.

like image 31
Ionut Robert Avatar answered Nov 12 '22 10:11

Ionut Robert


To solve jackson infinite recursion you can use @JsonManagedReference, @JsonBackReference.

@JsonManagedReference is the forward part of reference – the one that gets serialized normally.

@JsonBackReference is the back part of reference – it will be omitted from serialization.

Find more details here: http://www.baeldung.com/jackson-bidirectional-relationships-and-infinite-recursion

public class Student extends AbstractUser {
    @ManyToMany(fetch = FetchType.LAZY, targetEntity = Group.class)
    @JoinTable(name = "GROUPS_STUDENTS",
            joinColumns = { @JoinColumn(name = "student_id") },
            inverseJoinColumns = { @JoinColumn(name = "group_id") })
    @JsonManagedReference
    private List<Group> groups = new ArrayList<Group>();
}

public class Group implements Item, Serializable {
    @ManyToMany(mappedBy = "groups", targetEntity = Student.class)
    @JsonBackReference
    private List<Student> students;
}
like image 2
Abhilekh Singh Avatar answered Nov 12 '22 10:11

Abhilekh Singh


Or you can use DTO (Data Transfer Object) classes. These are plain code classes wich you can set to your needs when sendind data. For example, for your case you can have:

UserDTO.java:

import java.util.List;

public class UserDTO {

    private int id;

    private String password;

    private String firstName;

    private String lastName;

    private String email;

    private List<GroupDTO> groups;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public List<GroupDTO> getGroups() {
        return groups;
    }

    public void setGroups(List<GroupDTO> groups) {
        this.groups = groups;
    }

}

GroupDTO.java

package temp;

public class GroupDTO {

    private int id;

    private String name;

    private int year;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getYear() {
        return year;
    }

    public void setYear(int year) {
        this.year = year;
    }

}

This way you can customize the information you will send in the json file.

Let's say you have a User class and you want to send all users information:

    List<StudentDTO> response = new ArrayList<StudentDTO>(); //List to be send

    List<Student> students = bussinessDelegate.findAllStudents(); //Or whatever you do to get all students

    for (int i = 0; i < students.size(); i++) {

        Student student = students.get(i);          

        StudentDTO studentDTO = new StudentDTO(); //Empty constructor

        studentDTO.setEmail(student.getEmail());
        studentDTO.setFirstName(student.getFirstName());
        studentDTO.setLastName(student.getLastName());
        studentDTO.setId(student.getId());
        studentDTO.setPassword(student.getPassword());

        List<Group> studentGroups = student.getGroups();

        List<GroupDTO> studentGroupsDTO = new ArrayList<GroupDTO>();

        for (int j = 0; j < studentGroups.size(); j++) {

            Group group = studentGroups.get(j);

            GroupDTO groupDTO = new GroupDTO();

            groupDTO.setId(group.getId());
            groupDTO.setName(group.getName());
            groupDTO.setYear(group.getYear());

            studentGroupsDTO.add(groupDTO);

        }

        studentDTO.setGroups(studentGroupsDTO);
        response.add(studentDTO);
    }

    //Here you have your response list of students ready to send`
like image 1
Sebastiandg7 Avatar answered Nov 12 '22 10:11

Sebastiandg7