Джаксън - двупосочни отношения

1. Общ преглед

В този урок ще разгледаме най-добрите начини за справяне с двупосочните връзки в Джаксън .

Ще обсъдим проблема с безкрайната рекурсия на Jackson JSON, след това - ще видим как да сериализираме обекти с двупосочни връзки и накрая - ще ги десериализираме.

2. Безкрайна рекурсия

Първо - нека да разгледаме проблема с безкрайната рекурсия на Джаксън. В следващия пример имаме две обекти - " Потребител " и " Елемент " - с проста връзка едно към много :

Обектът „ Потребител “:

public class User { public int id; public String name; public List userItems; }

Обектът „ Артикул “:

public class Item { public int id; public String itemName; public User owner; }

Когато се опитаме да сериализираме екземпляр на „ Елемент “, Джаксън ще хвърли JsonMappingException изключение:

@Test(expected = JsonMappingException.class) public void givenBidirectionRelation_whenSerializing_thenException() throws JsonProcessingException { User user = new User(1, "John"); Item item = new Item(2, "book", user); user.addItem(item); new ObjectMapper().writeValueAsString(item); }

Най- пълен изключение е:

com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError) (through reference chain: org.baeldung.jackson.bidirection.Item["owner"] ->org.baeldung.jackson.bidirection.User["userItems"] ->java.util.ArrayList[0] ->org.baeldung.jackson.bidirection.Item["owner"] ->…..

Нека да видим, в течение на следващите няколко раздела - как да решим този проблем.

3. Използвайте @JsonManagedReference , @JsonBackReference

Първо, нека анотираме връзката с @JsonManagedReference , @JsonBackReference, за да позволим на Jackson да се справи по-добре с връзката:

Ето обекта „ Потребител “:

public class User { public int id; public String name; @JsonBackReference public List userItems; }

И „ Артикулът “:

public class Item { public int id; public String itemName; @JsonManagedReference public User owner; }

Нека сега тестваме новите обекти:

@Test public void givenBidirectionRelation_whenUsingJacksonReferenceAnnotation_thenCorrect() throws JsonProcessingException { User user = new User(1, "John"); Item item = new Item(2, "book", user); user.addItem(item); String result = new ObjectMapper().writeValueAsString(item); assertThat(result, containsString("book")); assertThat(result, containsString("John")); assertThat(result, not(containsString("userItems"))); }

Ето резултата от сериализацията:

{ "id":2, "itemName":"book", "owner": { "id":1, "name":"John" } }

Забележи, че:

  • @JsonManagedReference е препращащата част на препратката - тази, която се сериализира нормално.
  • @JsonBackReference е задната част на препратката - тя ще бъде пропусната от сериализацията.

4. Използвайте @JsonIdentityInfo

Сега - нека видим как да помогнем за сериализацията на обекти с двупосочна връзка, използвайки @JsonIdentityInfo .

Добавяме анотацията на ниво клас към нашия обект „ Потребител “:

@JsonIdentityInfo( generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") public class User { ... }

И към обекта „ Артикул “:

@JsonIdentityInfo( generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") public class Item { ... }

Време за теста:

@Test public void givenBidirectionRelation_whenUsingJsonIdentityInfo_thenCorrect() throws JsonProcessingException { User user = new User(1, "John"); Item item = new Item(2, "book", user); user.addItem(item); String result = new ObjectMapper().writeValueAsString(item); assertThat(result, containsString("book")); assertThat(result, containsString("John")); assertThat(result, containsString("userItems")); }

Ето резултата от сериализацията:

{ "id":2, "itemName":"book", "owner": { "id":1, "name":"John", "userItems":[2] } }

5. Използвайте @JsonIgnore

Като алтернатива можем също да използваме анотацията @JsonIgnore, за да просто игнорираме една от страните на връзката , като по този начин прекъсваме веригата.

В следния пример - ще предотвратим безкрайната рекурсия, като игнорираме свойството “ User ” “ userItems ” от сериализация:

Ето обект „ Потребител “:

public class User { public int id; public String name; @JsonIgnore public List userItems; }

И тук е нашият тест:

@Test public void givenBidirectionRelation_whenUsingJsonIgnore_thenCorrect() throws JsonProcessingException { User user = new User(1, "John"); Item item = new Item(2, "book", user); user.addItem(item); String result = new ObjectMapper().writeValueAsString(item); assertThat(result, containsString("book")); assertThat(result, containsString("John")); assertThat(result, not(containsString("userItems"))); }

И тук е резултатът от сериализацията:

{ "id":2, "itemName":"book", "owner": { "id":1, "name":"John" } }

6. Използвайте @JsonView

Можем да използваме и по- новата анотация @JsonView, за да изключим едната страна на връзката.

В следния пример - ние използваме два JSON изгледа - Public и Internal, където Internal се разширява Public :

public class Views { public static class Public {} public static class Internal extends Public {} }

Ще включим всички полета Потребител и Елемент в публичния изглед - с изключение на полето Потребител userItems, които ще бъдат включени във Вътрешния изглед:

Ето нашето лице „ Потребител “:

public class User { @JsonView(Views.Public.class) public int id; @JsonView(Views.Public.class) public String name; @JsonView(Views.Internal.class) public List userItems; }

И ето нашата субект „ Артикул “:

public class Item { @JsonView(Views.Public.class) public int id; @JsonView(Views.Public.class) public String itemName; @JsonView(Views.Public.class) public User owner; }

Когато сериализираме с помощта на публичния изглед, той работи правилно - защото изключихме userItems от сериализиране:

@Test public void givenBidirectionRelation_whenUsingPublicJsonView_thenCorrect() throws JsonProcessingException { User user = new User(1, "John"); Item item = new Item(2, "book", user); user.addItem(item); String result = new ObjectMapper().writerWithView(Views.Public.class) .writeValueAsString(item); assertThat(result, containsString("book")); assertThat(result, containsString("John")); assertThat(result, not(containsString("userItems"))); }

But If we serialize using an Internal view, JsonMappingException is thrown because all the fields are included:

@Test(expected = JsonMappingException.class) public void givenBidirectionRelation_whenUsingInternalJsonView_thenException() throws JsonProcessingException { User user = new User(1, "John"); Item item = new Item(2, "book", user); user.addItem(item); new ObjectMapper() .writerWithView(Views.Internal.class) .writeValueAsString(item); }

7. Use a Custom Serializer

Next – let's see how to serialize entities with bidirectional relationship using a custom serializer.

In the following example – we will use a custom serializer to serialize the “User” property “userItems“:

Here's the “User” entity:

public class User { public int id; public String name; @JsonSerialize(using = CustomListSerializer.class) public List userItems; }

And here is the “CustomListSerializer“:

public class CustomListSerializer extends StdSerializer
    
     { public CustomListSerializer() { this(null); } public CustomListSerializer(Class t) { super(t); } @Override public void serialize( List items, JsonGenerator generator, SerializerProvider provider) throws IOException, JsonProcessingException { List ids = new ArrayList(); for (Item item : items) { ids.add(item.id); } generator.writeObject(ids); } }
    

Let's now test out the serializer and see the right kind of output being produced:

@Test public void givenBidirectionRelation_whenUsingCustomSerializer_thenCorrect() throws JsonProcessingException { User user = new User(1, "John"); Item item = new Item(2, "book", user); user.addItem(item); String result = new ObjectMapper().writeValueAsString(item); assertThat(result, containsString("book")); assertThat(result, containsString("John")); assertThat(result, containsString("userItems")); }

And the final output of the serialization with the custom serializer:

{ "id":2, "itemName":"book", "owner": { "id":1, "name":"John", "userItems":[2] } }

8. Deserialize With @JsonIdentityInfo

Now – let's see how to deserialize entities with bidirectional relationship using @JsonIdentityInfo.

Here is the “User” entity:

@JsonIdentityInfo( generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") public class User { ... }

And the “Item” entity:

@JsonIdentityInfo( generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") public class Item { ... }

Let's now write a quick test – starting with some manual JSON data we want to parse and finishing with the correctly constructed entity:

@Test public void givenBidirectionRelation_whenDeserializingWithIdentity_thenCorrect() throws JsonProcessingException, IOException { String json = "{\"id\":2,\"itemName\":\"book\",\"owner\":{\"id\":1,\"name\":\"John\",\"userItems\":[2]}}"; ItemWithIdentity item = new ObjectMapper().readerFor(ItemWithIdentity.class).readValue(json); assertEquals(2, item.id); assertEquals("book", item.itemName); assertEquals("John", item.owner.name); }

9. Use Custom Deserializer

Finally, let's deserialize the entities with bidirectional relationship using a custom deserializer.

In the following example – we will use custom deserializer to parse the “User” property “userItems“:

Here's “User” entity:

public class User { public int id; public String name; @JsonDeserialize(using = CustomListDeserializer.class) public List userItems; }

And here is our “CustomListDeserializer“:

public class CustomListDeserializer extends StdDeserializer
    
     { public CustomListDeserializer() { this(null); } public CustomListDeserializer(Class vc) { super(vc); } @Override public List deserialize( JsonParser jsonparser, DeserializationContext context) throws IOException, JsonProcessingException { return new ArrayList(); } }
    

And the simple test:

@Test public void givenBidirectionRelation_whenUsingCustomDeserializer_thenCorrect() throws JsonProcessingException, IOException { String json = "{\"id\":2,\"itemName\":\"book\",\"owner\":{\"id\":1,\"name\":\"John\",\"userItems\":[2]}}"; Item item = new ObjectMapper().readerFor(Item.class).readValue(json); assertEquals(2, item.id); assertEquals("book", item.itemName); assertEquals("John", item.owner.name); }

10. Conclusion

В този урок илюстрирахме как да сериализираме / десериализираме обекти с двупосочни връзки, използвайки Jackson.

Внедряването на всички тези примери и кодови фрагменти може да бъде намерено в нашия проект GitHub - това е проект, базиран на Maven, така че трябва да е лесно да се импортира и да се изпълнява както е.