7

I'm struggling with Hibernate entities and JSON in these days and, although there is a lot of questions regarding the object, I'm yet not capable to serialize in presence of circular dependencies. I tried with both Gson and jackson but I didn't get a lot of progresses. Here is an excerpt from my objects. This is the "parent" class.

@Entity
public class User extends RecognizedServerEntities implements java.io.Serializable
{
    @Id
    @GeneratedValue(strategy = IDENTITY)
    @Column(name = "id", unique = true, nullable = false)
    private Integer id;

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "user", orphanRemoval = false)
    @Cascade({CascadeType.SAVE_UPDATE})
    private Set<Thread> threads = new HashSet<Thread>(0);
    //...other attributes, getters and setters
}

and this is the "children" class

@Entity
@Table(name = "thread")
public class Thread extends RecognizedServerEntities implements java.io.Serializable
{
    @Id
    @GeneratedValue(strategy = IDENTITY)
    @Column(name = "id", unique = true, nullable = false)
    private Integer id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "author", nullable = true)
    private User user;
    //...other attributes, getters and setters
}

I've written a simple class to test both gson and jackson features; as said, they both raise an exception.

public class MyJsonsTest
{
    private static User u;
    public static void main(String[] args)
    {
        u = new User("mail", "password", "nickname", new Date());
        u.setId(1); // Added with EDIT 1
    //  testGson();
        testJackson();
    }

    private static void testJackson()
    {
        Thread t = new Thread("Test", u, new Date(), new Date());
        t.setId(1); // Added with EDIT 1
        u.getThreads().add(t);

        ObjectMapper mapper = new ObjectMapper();
        mapper.enable(SerializationFeature.INDENT_OUTPUT);
        try
        {
            mapper.writeValue(new File("result.json"), u);
        }
        catch {/[various exceptions catched, but a JsonMappingException was thrown]}
    }

    private static void testGson()
    {
        Gson gson = new Gson();
        System.out.println(u.toString());
        System.out.println(gson.toJson(u, User.class));

        Thread t = new Thread("Test", u, new Date(), new Date());
        u.getThreads().add(t);

        //This raise an exception overflow
        System.out.println(gson.toJson(u, User.class));
    }
}

To solve the problem, on jackson side, I tried to use this annotation

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

on both User and Thread class. However, it doesn't solve the problem. On gson side, I read about the GraphAdapterBuilder class, but I wasn't able to properly use it. I don't find any jar, so I copy/pasted the source code from here. However, there is a compile time error at this line

 private final ConstructorConstructor constructorConstructor = new ConstructorConstructor();

because the ConstructorConstructor() is undefined; the right syntax should be

ConstructorConstructor(Map<Type>, InstanceCreator<?> instanceCreators)

So, is there a definitive solution to this problem? Obviously, I can't use transient variables.

EDIT 1

I finally found the issue with jackson. In the test class, I forgot to initialize the id field (in real scenarios it is initialized by the database) and this is the reason of the exception. When I finally set the id, all works. This is the output

{
  "id" : 1,
  "email" : "mail",
  "password" : "password",
  "nick" : "nickname",
  "registeredDate" : 1414703168409,
  "threads" : [ {
    "id" : 1,
    "thread" : null,
    "user" : 1,
    "title" : "Test",
    "lastModifiedDate" : 1414703168410,
    "createdDate" : 1414703168410,
    "messages" : [ ],
    "threads" : [ ]
  } ],
  "messages" : [ ]
}
tigerjack
  • 1,158
  • 3
  • 21
  • 39
  • 2
    In my case, I create specific DTOs to transfer the data stored in Hibernate Objects to JSON and I avoid this kind of two-way relationships in my design. Or, on the other hand, I do not use Hibernate with JSON at all. – Luiggi Mendoza Oct 30 '14 at 15:37
  • Why is the relationship one way mapped by "user" and the other way by "author"? – BarrySW19 Oct 30 '14 at 15:48
  • @LuiggiMendoza which kind of DTOs have you used? – tigerjack Oct 30 '14 at 15:56
  • I create a new class Hibernate/JPA/Any-other-framework-less annotated, a plain POJO with the necessary fields to communicate. It's more work, you could even say duplicate efforts, but it's a trade off you assume when trying to work with these technologies. The other solution would be using a proper design where you don't involve circular references in your objects. – Luiggi Mendoza Oct 30 '14 at 15:57
  • @BarrySW19 don't know, haven't look at it before. Hibernate automatically generated the entities and the annotations with reverse engineering. I thought it's something like `Thread.author` refers to `User.id`, but I'm not sure about it. – tigerjack Oct 30 '14 at 16:00
  • @LuiggiMendoza I get your point, but I thought that there was a much better solution. Take a look at [this](http://java.dzone.com/articles/circular-dependencies-jackson) for example; it assures that there is a solution with jackson. Even GraphAdapterBuilder promises to be solution for gson circular dependencies. However, I can't be able to use it and I think that I'm missing something. – tigerjack Oct 30 '14 at 16:10
  • @tigerjack89 why aren't able to use it? The solution provided on the link seems pretty straightforward. – Luiggi Mendoza Oct 30 '14 at 17:42
  • @LuiggiMendoza I don't know. I tried to add the suggested annotation to all the entities. However, when I run the test code, I always get an exception. – tigerjack Oct 30 '14 at 19:02
  • I suggest you to isolate the problem and try doing in groups or 2 or 3. – Luiggi Mendoza Oct 30 '14 at 19:02
  • @LuiggiMendoza well, as you can see from the above code, I'm just trying to 1. create an user; 2. create a thread (with an user assigned to it); 3. add the thread to user threads; 4. serialize the user writing it to a file (jackson) or the output stream (gson). In both cases, it throws an exception. What do you think I can do to isolate the problem? – tigerjack Oct 30 '14 at 20:29
  • Ok, since this is your exact problem, then you already have it isolated. Now, start working with the proposed solution in the link you posted, and start the test-fail-test-succeed process. If you still need help probably somebody else could provide an answer to this question. Since I'm also interested in getting an answer to this question, if in some days there aren't responses then I'll provide a bounty. – Luiggi Mendoza Oct 30 '14 at 20:33
  • 1
    @LuiggiMendoza thanks for your help. Actually, I made some tests with other simple classes and indeed it seems to work. Also, I was wondering if the problem with my entities is with some sort of "internal reference"; I mean, my `Thread` entity is linked to a collection of sub-threads. So, I also made some other tests, adding to `ParentEntity` (I'm using the names provided in the link) the fields `List parentEntities` and `ParentEntity parentEntity`. Again, the test was passed. So, I have to look closely to my class. – tigerjack Oct 30 '14 at 20:56
  • found the issue with jackson! I haven't initialized the id field (in real scenarios it is initialized by the database) and this is the reason of the exception. When I finally set the id, all works. ...Editing OQ and testing other stuffs... – tigerjack Oct 30 '14 at 21:10
  • For Gson - you can use Exclusion strategy class described in this [thread](https://stackoverflow.com/questions/43592368/exclude-remove-field-from-parsed-json) – Simas Paškauskas Oct 27 '21 at 07:48

4 Answers4

5

Jackson

As said, I was able to solve the problem using

@JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="id", scope=MyEntity.class)` 

for each entity as suggested here. The scope attribute was necessary to make sure that the name "id" is unique within the scope. Actually, without the scope attribute, as you can see here, it throws an exception saying

com.fasterxml.jackson.databind.JsonMappingException: Already had POJO for id java.lang.String) [com.fasterxml.jackson.annotation.ObjectIdGenerator$IdKey@3372bb3f] (through reference chain: ParentEntity["children"]->java.util.ArrayList[0]->ChildEntity["id"])
...stacktrace...
Caused by: java.lang.IllegalStateException: Already had POJO for id (java.lang.String) [com.fasterxml.jackson.annotation.ObjectIdGenerator$IdKey@3372bb3f]
...stacktrace...

Gson

I still haven't found a clean way to serialize circular dependencies.

Community
  • 1
  • 1
tigerjack
  • 1,158
  • 3
  • 21
  • 39
  • for Gson you can use ExclusionStrategy mention here (just exclude the field that causes circular dependency): https://stackoverflow.com/questions/43592368/exclude-remove-field-from-parsed-json A bit of a Hack, but works like a charm – Simas Paškauskas Oct 27 '21 at 07:50
4

When dealing with circular dependencies you need to build a parent-children JSON hierarchy, because the marshalling must be cascaded from root to the inner-most child.

For bi-directional associations, when the Parent has a one-to-many children collection and the child has a many-to-one reference to Parent, you need to annotate the many-to-one side with @JsonIgnore:

@Entity
@Table(name = "thread")
public class Thread extends RecognizedServerEntities implements java.io.Serializable
{
    @Id
    @GeneratedValue(strategy = IDENTITY)
    @Column(name = "id", unique = true, nullable = false)
    private Integer id;

    @org.codehaus.jackson.annotate.JsonIgnore
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "author", nullable = true)
    private User user;
    //...other attributes, getters and setters
}

This way you will no longer have a Json serializing-time circular dependency.

Vlad Mihalcea
  • 142,745
  • 71
  • 566
  • 911
  • Thanks for your help :) I partially solved the problem using @JsonIdentityInfo, as you can see from one of the answer. So, what do you think are the best choice here? Why is JsonIgnore preferable to JsonIdentityInfo in this situation? What are the many advantages/drawbacks of the two choices? – tigerjack Nov 26 '14 at 11:27
  • As long as it ignores the duplicated info both solutions yield the same result. – Vlad Mihalcea Nov 26 '14 at 13:53
  • Sure, I was asking because I'm having other issues, in which JsonIdentityInfo doesn't work as expected, while JsonIgnore is unusable because "nullable=false" option. I thought that there was specific problems in the use of one of another. – tigerjack Nov 26 '14 at 16:26
1

I have done this using org.codehaus.jackson.annotate.JsonManagedReference and org.codehaus.jackson.annotate.JsonBackReference in this way...

look at how i used @JsonManagedReference

 @Id
 @TableGenerator(name="CatId", table="catTable",pkColumnName="catKey",pkColumnValue="catValue", allocationSize=1)
 @GeneratedValue(strategy=GenerationType.TABLE, generator="CatId")
 @Column(name = "CategId", unique = true, nullable = false)
 private long categoryId;
 private String productCategory;
 @JsonManagedReference("product-category")
 @OneToMany(targetEntity=ProductDatabase.class,mappedBy="category", cascade=CascadeType.ALL, fetch=FetchType.EAGER)
 private List<ProductDatabase> catProducts;

and then at the other end i used @JsonBackReference as shown below.

@Id@GeneratedValue
private int productId;
private String description;
private int productPrice;
private String productName;
private String ProductImageURL;
@JsonBackReference("product-category")
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "CategId")
private Category category;

just apply these annotations and check if it works for you.

Mayur Gupta
  • 762
  • 3
  • 8
  • 23
0

Its not good design to serialize Hibernate POJO to client. As you may send some data to client location, which he is not authorize to view. You should create client POJO and copy data from hibernate POJO to client POJO, which you want to send to client. If you don't want to do that, you can use @JsonIgnore or Fetch all data eagerly.

Pavan
  • 121
  • 4