Limpan
Limpan

Reputation: 3

Jackson Annotations not working in Spring Boot

I'm encountering a challenge with Jackson annotations in my User class while managing a self-referencing @ManyToMany relationship. Despite applying the appropriate annotations, Jackson fails to handle the relationship correctly. When I retrieve the User entity, the followers and following fields enter a recursive loop, leading to an infinite cycle of references.

Here’s the User entity:

import com.fasterxml.jackson.annotation.*;
import com.pubfinder.pubfinder.models.enums.Role;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;

/**
 * The type User.
 */
@Entity
@Table(name = "users")
@AllArgsConstructor
@NoArgsConstructor
@Builder
@JsonIdentityInfo(generator = ObjectIdGenerators.UUIDGenerator.class, property = "id")
public class User {
    @Id
    @GeneratedValue
    @Column(unique = true, nullable = false)
    private UUID id;
    @Column(unique = true, nullable = false)
    private String username;
    private String firstname;
    private String lastname;
    @Column(unique = true, nullable = false)
    private String email;
    @Column(nullable = false)
    private String password;
    @Column(nullable = false)
    @Enumerated(EnumType.ORDINAL)
    private Role role;

    @ManyToMany(
            fetch = FetchType.LAZY,
            cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH}
    )
    @JoinTable(
            name = "user_following",
            joinColumns = @JoinColumn(name = "user_id"),
            inverseJoinColumns = @JoinColumn(name = "following_id")
    )
    @Builder.Default
    @JsonManagedReference
    Set<User> following = new HashSet<>();

    @ManyToMany(
            mappedBy = "following",
            fetch = FetchType.LAZY,
            cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH}
    )
    @Builder.Default
    @JsonBackReference
    Set<User> followers = new HashSet<>();

    @PreRemove
    private void cleanupFollowerRelationshipsBeforeDeletion() {
        for (User user : this.following) {
            user.getFollowers().remove(this);
        }
    }

    public void addFollowing(User user) {
        if (!this.following.contains(user)) {
            this.following.add(user);
            user.getFollowers().add(this);
        }
    }

    public void removeFollowing(User user) {
        if (this.following.contains(user)) {
            this.following.remove(user);
            user.getFollowers().remove(this);
        }
    }

    // Getters and Setters......
}

Dependencies: Here are the main dependencies in my build.gradle file:

implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("com.fasterxml.jackson.core:jackson-annotations:2.15.4")
implementation("org.projectlombok:lombok")
annotationProcessor("org.projectlombok:lombok")
testImplementation("com.h2database:h2")

Jackson should handle the bidirectional relationships using @JsonManagedReference and @JsonBackReference correctly, and the serialization/deserialization process should avoid infinite recursion.

I have also tried @JsonIdentityInfo and @JsonIgnore, but nothing seems to work.

This is how I want the output to look:

{
  "id": "1",
  "username": "user1",
  "following": [
    {
      "id": "2",
      "username": "user2",
      "following": [
        {
          "id": "1",
          "username": "user1",
          "following": [ /* infinite nesting */ ]
        }
      ]
    }
  ]
}

This how I want it to look:

{
  "id": "1",
  "username": "user1",
  "following": [
    { "id": "2", "username": "user2" }
  ],
  "followers": [
    { "id": "3", "username": "user3" }
  ]
}

Upvotes: 0

Views: 58

Answers (1)

Here is the solution shown below

Using DTOs (Data Transfer Objects) is an effective way to handle self-referencing relationships in your User entity and avoid infinite recursion during serialization.

1 ) Create DTO Classes

UserBasicDTO.java

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserBasicDTO {
    private UUID id;
    private String username;

    public static UserBasicDTO fromEntity(User user) {
        return new UserBasicDTO(
            user.getId(),
            user.getUsername()
        );
    }
}

UserDTO.java

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserDTO {
    private UUID id;
    private String username;
    private String firstname;
    private String lastname;
    private String email;

    private Set<UserBasicDTO> following;
    private Set<UserBasicDTO> followers;

    public static UserDTO fromEntity(User user) {
        return new UserDTO(
            user.getId(),
            user.getUsername(),
            user.getFirstname(),
            user.getLastname(),
            user.getEmail(),
            user.getFollowing().stream()
                .map(UserBasicDTO::fromEntity)
                .collect(Collectors.toSet()),
            user.getFollowers().stream()
                .map(UserBasicDTO::fromEntity)
                .collect(Collectors.toSet())
        );
    }
}

2 ) Define UserService

@Service
@Transactional
public class UserService {

    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public UserDTO getUserById(UUID id) {
        return userRepository.findById(id)
                .map(UserDTO::fromEntity)
                .orElseThrow(() -> new RuntimeException("User not found"));
    }


    public List<UserDTO> getAllUsers() {
        return userRepository.findAll().stream()
                .map(UserDTO::fromEntity)
                .collect(Collectors.toList());
    }

}

3 ) Define Controller

@RestController
@RequestMapping("/api/users")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/{id}")
    public ResponseEntity<UserDTO> getUser(@PathVariable UUID id) {
        UserDTO userDTO = userService.getUserById(id);
        return ResponseEntity.ok(userDTO);
    }


    @GetMapping
    public ResponseEntity<List<UserDTO>> getAllUsers() {
        List<UserDTO> users = userService.getAllUsers();
        return ResponseEntity.ok(users);
    }

}

Upvotes: 0

Related Questions