1

json recursion is a known issue with bi-directional relations in JPA. There are many solutions, like using @JsonIdentityInfo and @JsonManagedReference and @JsonBackReference, but all they end up doing is not returning the specific node in the json itself. My need is little different, where I do want the node to be returned but only certain fields, and not the node that causes the recursion.

To keep things simple, the tables look like this, I have 2 tables, user and article. One user can have multiple articles (the business case is little different, just modifying it here to keep things simple)

User:
user_id, first_name, last_name

Article:
article_id, article_text, user_id (foreign key to User table)

The Entity classes look like this:

@Entity
@Table (name = "user")
@Data  // lombok to generate corresponding getters and setters
public class User
{
   private int userId;
   private String firstName;
   private String lastName;

   @OneToMany ()
   @JoinColumn (name="user")
   private List<Article> articles;
}

@Entity
@Table (name = "article")
@Data  // lombok to generate corresponding getters and setters
public class Article
{
   private int articleId;
   private String articleText;

   @ManyToOne ()
   @JoinColumn (name="user_id")
   private User user;
}

The problem occurs when I make a call to get User using findById (), JPA internally populates "articles" as well (it might be Lazy but if I convert the object into a JSON string, it gets populated). Each article in the list has reference back to "user" object which it populates too, which again has "articles" and this goes on causing recursion and StackOverflowException.

My need is that in this particular case, when JPA is fetching and populating User object for a given article, I want only firstName and lastName to be populated and NOT the articles field. JPA is running those join queries internally while fetching articles, and I am not calling it myself. Note, I am just calling userRepository.findById () method.

I want JSON representation to look like this (Note that the first user node includes everything, including articles, but user node that's fetched along with article should NOT contains articles):

{
  "userId": 1,
  "firstName": "John",
  "lastName": "Doe",
  "articles": [
    {
       "articleId": 100,
       "articleText": "Some sample text",
       "user": {
          "userId": 1,
          "firstName": "John",
          "lastName": "Doe"
       }
    },
    {
       "articleId": 101,
       "articleText": "Another great article",
       "user": {
          "userId": 1,
          "firstName": "John",
          "lastName": "Doe"
       }
    }
  ]
}

Is this possible?

Vasily Liaskovsky
  • 2,248
  • 1
  • 17
  • 32
AC1
  • 463
  • 3
  • 9
  • Try jackson JSON views. – Vasily Liaskovsky Jun 26 '23 at 20:15
  • Sometimes the easy solution is to map the persistence objects onto simple record types and serialize those instead. – Deadron Jun 26 '23 at 20:31
  • @Deadron, that may not be a good solution, as even a simple System.out.println (user) will end up causing StackOverflow. Point is, the solution can't be that don't print or try to serialize or even debug otherwise you'll get an exception. – AC1 Jun 26 '23 at 22:13
  • You can't make non trivial JPA objects be foolproof. You can try but it never works. This is why its a bad idea to serialize them directly – Deadron Jun 27 '23 at 19:40

3 Answers3

0

first, you add @JsonIgnore on articles property of User like this

   @JsonIgnore
   @OneToMany ()
   @JoinColumn (name="user")
   private List<Article> articles;

this solves the Json problem and will generate the required result,

and then, you override toString() of User Entity and don't include articles in it. that will prevent stackoverflow when printing it to the console

Ahmed Nabil
  • 457
  • 1
  • 9
  • AhmedNabil, I don't want to ignore the whole articles property, rather I do want it in the result json. I also want the user object within article to be there in json too, except that for this user object I only want firstName and lastName and not articles field. – AC1 Jun 27 '23 at 22:06
  • as far as I know, that is not applicable in Jackson with Hibernate. also why would you want to include articles each time you fetch user data ? it's preferred to fetch user data first, then you can use user Id to hit articles endpoint and fetch articles when needed – Ahmed Nabil Jun 27 '23 at 23:29
0

The JPA specification allows us to customize results in an object-oriented fashion. Therefore, we can use a JPQL constructor expression to set the result, using the Select NEW :

@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class Comment {
    @Id
    private Integer id;
    private Integer year;
    private boolean approved;
    private String content; 
}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommentCount {
    private Integer year;
    private Long total;
}


@Query("SELECT new CommentCount(c.year, COUNT(c.year)) "
  + "FROM Comment AS c GROUP BY c.year ORDER BY c.year DESC")
List<CommentCount> countTotalCommentsByYearClass();
Tiago Medici
  • 1,944
  • 22
  • 22
  • in my case, it's not me calling a specific repository method to get the results where I can skip certain columns, JPA is doing that on its own since articles is an object inside user object and then user is an object inside article object. – AC1 Jun 27 '23 at 22:05
0

I ended up refactoring the code. Separated out Entity classes and serialized classes, i.e. the class that fetches data from DB via JPA and the class that returns json back in the API. Thanks everyone for the answers and nudges.

AC1
  • 463
  • 3
  • 9