知識庫 / JSON / Jackson RSS 訂閱

Jackson – 雙向關係

Data,Jackson
HongKong
6
10:03 PM · Dec 05 ,2025

1. 概述

在本教程中,我們將探討在 Jackson 中處理 雙向關係的最佳方法。

首先,我們將討論 Jackson 中的 JSON 無限遞歸問題。然後我們將看到如何級聯序列化具有雙向關係的實體。最後,我們將反序列化它們。

2. 無限遞歸

讓我們來看一下 Jackson 的無限遞歸問題。在下面的示例中,我們有兩個實體:“User” 和 “Item” ,它們之間存在 簡單的一對多關係

User” 實體:

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,”實例時,Jackson 會拋出 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 更好地處理該關係:

以下是 “User” 實體:

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 對象包含對 User 的引用,則需要使用 @JsonIdentityInfo。 我們將在下一部分中對此進行探討。

4. 使用 @JsonIdentityInfo

現在,讓我們學習如何使用 @JsonIdentityInfo 來幫助處理具有雙向關係的實體序列化。

我們將添加類級別的註解到我們的 “User” 實體中:

@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 視圖,PublicInternal,其中 Internal 繼承了 Public

public class Views {
    public static class Public {}

    public static class Internal extends Public {}
}

我們將包含所有 UserItem 字段在 Public 視圖中 ,除了 User 字段 userItems,該字段將在 Internal 視圖中包含:

以下是我們的 “User” 實體:

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;
}

以下是我們的“Item”實體:

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")));
}

如果使用內部視圖進行序列化,則會拋出 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. 使用自定義序列化器

接下來,我們將看到如何使用自定義序列化器來序列化具有雙向關係的事實實體。

在下面的示例中,我們將使用自定義序列化器來序列化“User”實體的“userItems”屬性。

以下是“User”實體:

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. 使用 <em @JsonIdentityInfo 進行反序列化

現在,讓我們看看如何使用 <em @JsonIdentityInfo 反序列化具有雙向關係的實體。

以下是 "<em User" 實體:

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

以及“Item”實體:

@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 異常

當我們在同一個實體中使用多重雙向關係時,可能會在序列化過程中遇到異常:

com.fasterxml.jackson.databind.JsonMappingException: Multiple back-reference properties with name 'defaultReference'

這主要是因為 Jackson 會為每個 @JsonBackReference 默認指定引用名稱為 ‘defaultReference’,除非我們明確提供一個值。 如果一個實體擁有多個指向自身的 back-reference 且沒有指定唯一名稱,Jackson 將無法區分它們。

假設我們有一個 User 實體,它擁有兩個列表:wishlistsoldItems。這兩個列表都指向該 User 實體作為所有者。

public class User {
    public int id;
    public String name;

    @JsonManagedReference
    public List wishlist = new ArrayList<>();

    @JsonManagedReference
    public List soldItems = new ArrayList<>();
}

public class Item {
    public int id;
    public String itemName;

    @JsonBackReference
    public User owner;
}

這將導致 Jackson 在序列化過程中拋出 JsonMappingException,因為它無法區分具有相同默認名稱的多重反向引用。

為了解決這個問題,我們需要為每對引用提供一個唯一的名稱,通過 value 屬性:

public class User {
    public int id;
    public String name;

    @JsonManagedReference(value="wishlistRef")
    public List wishlist = new ArrayList<>();

    @JsonManagedReference(value="soldItemsRef")
    public List soldItems = new ArrayList<>();
}

public class Item {
    public int id;
    public String itemName;

    @JsonBackReference(value="wishlistRef")
    public User wishlistOwner;

    @JsonBackReference(value="soldItemsRef")
    public User soldOwner;
}

現在,Jackson 已經知道每個管理引用與哪個反向引用相對應,序列化也正常工作。

我們可以編寫一個測試示例,演示如何使用名為 @JsonManagedReference@JsonBackReference 的註解,允許 Jackson 正確地序列化具有多個反向引用且不會拋出異常的對象:

@Test
public void givenMultipleBackReferencesOnWishlist_whenNamedReference_thenNoException() throws JsonProcessingException {
    User user = new User();
    user.id = 1;
    user.name = "Alice";

    Item item1 = new Item();
    item1.id = 101;
    item1.itemName = "Book";
    item1.wishlistOwner = user;

    Item item2 = new Item();
    item2.id = 102;
    item2.itemName = "Pen";
    item2.wishlistOwner = user;

    user.wishlist = List.of(item1, item2);

    Item item3 = new Item();
    item3.id = 201;
    item3.itemName = "Laptop";
    item3.soldOwner = user;

    Item item4 = new Item();
    item4.id = 202;
    item4.itemName = "Phone";
    item4.soldOwner = user;

    user.soldItems = List.of(item3, item4);

    String json = new ObjectMapper().writeValueAsString(user);
    assertThat(json, containsString("Alice"));
    assertThat(json, containsString("Book"));
    assertThat(json, containsString("Pen"));
}

11. 結論

在本文中,我們演示瞭如何使用 Jackson 對具有雙向關係的事實實體進行序列化/反序列化。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.