Open gilberto-torrezan opened 7 years ago
Is there any real-world case when this happens, or is it only a theoretical issue?
I also run into this issue when using a bidirectional JPA relationship. In the following example with collections, but it's the same issue.
```java
@Entity
@Table(name = "recipe")
public class Recipe implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotNull
@Column(name = "name", nullable = false, unique = true)
private String name;
@OneToMany(mappedBy = "recipe")
private Set
A simple test to show the error:
public class SerializeTest {
@Test
public void shouldSerialize() {
Recipe r = new Recipe();
RecipeItem ri = new RecipeItem();
ri.setRecipe(r);
r.setRecipeItems(Sets.newHashSet(ri));
ri.setRecipe(r);
r.addRecipeItems(ri);
String json = JsonSerializer.toJson(r).toJson(); // StackOverflowError exception
System.out.println(json);
assertTrue(true);
}
}
Note the cycle ```java java.lang.StackOverflowError at sun.reflect.misc.ReflectUtil.checkPackageAccess(ReflectUtil.java:164) at sun.reflect.misc.ReflectUtil.isPackageAccessible(ReflectUtil.java:195) at java.beans.Introspector.getBeanInfo(Introspector.java:164) at com.vaadin.flow.internal.JsonSerializer.toJson(JsonSerializer.java:83) at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193) at java.util.HashMap$KeySpliterator.forEachRemaining(HashMap.java:1556) at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481) at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471) at java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151) at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174) at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) at java.util.stream.ReferencePipeline.forEachOrdered(ReferencePipeline.java:423) at com.vaadin.flow.internal.JsonSerializer.toJson(JsonSerializer.java:120) at com.vaadin.flow.internal.JsonSerializer.toJson(JsonSerializer.java:67) at com.vaadin.flow.internal.JsonSerializer.toJson(JsonSerializer.java:90) at com.vaadin.flow.internal.JsonSerializer.toJson(JsonSerializer.java:90) at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193) at java.util.HashMap$KeySpliterator.forEachRemaining(HashMap.java:1556) at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481) at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471) at java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151) at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174) at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) at java.util.stream.ReferencePipeline.forEachOrdered(ReferencePipeline.java:423) at com.vaadin.flow.internal.JsonSerializer.toJson(JsonSerializer.java:120) at com.vaadin.flow.internal.JsonSerializer.toJson(JsonSerializer.java:67) at com.vaadin.flow.internal.JsonSerializer.toJson(JsonSerializer.java:90) at com.vaadin.flow.internal.JsonSerializer.toJson(JsonSerializer.java:90) at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193) at java.util.HashMap$KeySpliterator.forEachRemaining(HashMap.java:1556) at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481) at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471) ```
Jackson for example provides JsonIgnoreProperties
to break the cycle.
I can't believe the issue is still present. This would mean there is nobody using bidirectional associations in any of their objects that go to the UI or it would mean they all map to DTO before.
What would you expect the JSON to look like?
I used the following test with jackson mapper to produce the json I would expect.
public class SerializeTest {
@Test
public void shouldSerialize() throws JsonProcessingException {
Recipe r = new Recipe();
r.setId(42L);
r.setName("My Recipe");
RecipeItem ri = new RecipeItem();
ri.setId(1L);
ri.setRecipe(r);
r.setRecipeItems(Sets.newHashSet(ri));
r.addRecipeItems(ri);
final ObjectMapper objectMapper = new ObjectMapper();
String jsonJackson = objectMapper.writeValueAsString(r);
System.out.println(jsonJackson); // {"id":42,"name":"My Recipe","recipeItems":[{"id":1,"recipe":{"id":42,"name":"My Recipe"}}]}
String json = JsonSerializer.toJson(r).toJson(); // StackOverflowError exception
System.out.println(json);
assertTrue(true);
}
}
{
"id": 42,
"name": "My Recipe",
"recipeItems": [
{
"id": 1,
"recipe": {
"id": 42,
"name": "My Recipe"
}
}
]
}
I don't understand the logic for that JSON output. Why is the the item with id 42
duplicated but not the item with id 1
?
That's because of the @JsonIgnoreProperties
annotation in RecipeItem
- otherwise it would produce a cycle.
@JsonIgnoreProperties(value = {"recipeItems"})
private Recipe recipe;
It tells jackson to omit the recipeItems-property when serializing the recipe.
If you omit the annotation jackson fails with an exception that explains the cycle:
com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError)
(through reference chain: RecipeItem["recipe"]->Recipe["recipeItems"]->java.util.HashSet[0]->RecipeItem["recipe"]->Recipe["recipeItems"]->java.util.HashSet[0]->RecipeItem["recipe"] ...
I hope this helps.
Example object:
Currently the serialization enters in an infinite loop that causes a StackOverflow when trying to convert the object to json.