«1. Обзор
В этом уроке мы рассмотрим лучшие способы работы с двунаправленными отношениями в Джексоне.
Мы обсудим проблему бесконечной рекурсии Джексона JSON, затем — мы увидим, как сериализовать сущности с двунаправленными отношениями и, наконец — мы их десериализуем.
2. Бесконечная рекурсия
Во-первых, давайте рассмотрим проблему бесконечной рекурсии Джексона. В следующем примере у нас есть две сущности — «Пользователь» и «Элемент» — с простым отношением «один ко многим»:
Сущность «Пользователь»:
public class User {
public int id;
public String name;
public List<Item> userItems;
}
Сущность «Item»:
public class Item {
public int id;
public String itemName;
public User owner;
}
Когда мы пытаемся сериализовать экземпляр «Item», Джексон выдает исключение 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, чтобы позволить Джексону лучше обрабатывать отношения:
Вот объект «Пользователь»:
public class User {
public int id;
public String name;
@JsonManagedReference
public List<Item> userItems;
}
~ ~~ И «Item»:
public class Item {
public int id;
public String itemName;
@JsonBackReference
public User owner;
}
Давайте теперь протестируем новые сущности:
@Test
public void givenBidirectionRelation_whenUsingJacksonReferenceAnnotationWithSerialization_thenCorrect() throws JsonProcessingException {
final User user = new User(1, "John");
final Item item = new Item(2, "book", user);
user.addItem(item);
final String itemJson = new ObjectMapper().writeValueAsString(item);
final String userJson = new ObjectMapper().writeValueAsString(user);
assertThat(itemJson, containsString("book"));
assertThat(itemJson, not(containsString("John")));
assertThat(userJson, containsString("John"));
assertThat(userJson, containsString("userItems"));
assertThat(userJson, containsString("book"));
}
Вот результат сериализации объекта Item:
{
"id":2,
"itemName":"book"
}
А вот вывод сериализации объекта User:
{
"id":1,
"name":"John",
"userItems":[{
"id":2,
"itemName":"book"}]
}
Обратите внимание, что:
-
@JsonManagedReference — это прямая часть ссылки — та, которая обычно сериализуется. @JsonBackReference — это задняя часть ссылки — она будет исключена из сериализации. Сериализованный объект Item не содержит ссылки на объект User.
Также обратите внимание, что мы не можем переключаться между аннотациями. Для сериализации будет работать следующее:
@JsonBackReference
public List<Item> userItems;
@JsonManagedReference
public User owner;
Но при попытке десериализации объекта будет выдано исключение, поскольку @JsonBackReference нельзя использовать в коллекции.
Если мы хотим, чтобы сериализованный объект Item содержал ссылку на пользователя, нам нужно использовать @JsonIdentityInfo. Мы рассмотрим это в следующем разделе.
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» при сериализации:
Вот объект «User»:
public class User {
public int id;
public String name;
@JsonIgnore
public List<Item> 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 {}
}
Мы включим все поля User и Item в Public View — кроме Пользовательское поле userItems, которое будет включено во внутреннее представление:
Вот наша сущность «Пользователь»:
public class User {
@JsonView(Views.Public.class)
public int id;
@JsonView(Views.Public.class)
public String name;
@JsonView(Views.Internal.class)
public List<Item> 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;
}
Когда мы сериализуем, используя представление Public, оно работает правильно, потому что мы исключили 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")));
}
Но если мы сериализуем, используя представление Internal, выбрасывается JsonMappingException, потому что включены все поля:
@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. Использование пользовательского сериализатора
Далее — давайте посмотрим, как сериализовать сущности с двунаправленными отношениями с помощью пользовательского сериализатора.
В следующем примере мы будем использовать пользовательский сериализатор для сериализации свойства «Пользователь» «userItems»:
Вот объект «Пользователь»:
public class User {
public int id;
public String name;
@JsonSerialize(using = CustomListSerializer.class)
public List<Item> userItems;
}
А вот «CustomListSerializer»:
public class CustomListSerializer extends StdSerializer<List<Item>>{
public CustomListSerializer() {
this(null);
}
public CustomListSerializer(Class<List> t) {
super(t);
}
@Override
public void serialize(
List<Item> items,
JsonGenerator generator,
SerializerProvider provider)
throws IOException, JsonProcessingException {
List<Integer> ids = new ArrayList<>();
for (Item item : items) {
ids.add(item.id);
}
generator.writeObject(ids);
}
}
Теперь давайте протестируем сериализатор и посмотрим, какой результат получается:
@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"));
}
И окончательный результат сериализации с пользовательским сериализатором:
{
"id":2,
"itemName":"book",
"owner":
{
"id":1,
"name":"John",
"userItems":[2]
}
}
8. Десериализация с помощью @JsonIdentityInfo
Теперь давайте посмотрим, как десериализовать сущности с двунаправленными отношениями с помощью @JsonIdentityInfo.
«Вот объект «Пользователь»:
@JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id")
public class User { ... }
И объект «Элемент»:
@JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id")
public class Item { ... }
Давайте теперь напишем быстрый тест — начнем с некоторых данных JSON, которые мы хотим проанализировать, и закончим с правильно сконструированной сущностью:
@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. Используйте пользовательский десериализатор
Наконец, давайте десериализуем сущности с двунаправленной связью, используя пользовательский десериализатор.
В следующем примере мы будем использовать пользовательский десериализатор для анализа свойства «User» «userItems»:
Вот объект «User»:
public class User {
public int id;
public String name;
@JsonDeserialize(using = CustomListDeserializer.class)
public List<Item> userItems;
}
А вот наш «CustomListDeserializer»:
public class CustomListDeserializer extends StdDeserializer<List<Item>>{
public CustomListDeserializer() {
this(null);
}
public CustomListDeserializer(Class<?> vc) {
super(vc);
}
@Override
public List<Item> deserialize(
JsonParser jsonparser,
DeserializationContext context)
throws IOException, JsonProcessingException {
return new ArrayList<>();
}
}
И простой тест:
@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. Заключение
В этом руководстве мы продемонстрировали, как сериализовать/десериализовать объекты с двунаправленными отношениями с помощью Jackson.
Реализацию всех этих примеров и фрагментов кода можно найти в нашем проекте GitHub — это проект на основе Maven, поэтому его должно быть легко импортировать и запускать как есть.