Jackson - Relacionamentos bidirecionais

Jackson - Relacionamentos bidirecionais

1. Visão geral

Neste tutorial, examinaremos as melhores maneiras de lidar combidirectional relationships in Jackson.

Discutiremos o problema de recursão infinita Jackson JSON, então - veremos como serializar entidades com relacionamentos bidirecionais e, finalmente, iremos desserializá-los.

2. Recursão infinita

Primeiro - vamos dar uma olhada no problema de recursão infinita de Jackson. No exemplo a seguir, temos duas entidades - “User” e “Item” - coma simple one-to-many relationship:

A entidade “User”:

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

A entidade “Item”:

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

Quando tentamos serializar uma instância de “Item“, Jackson lançará uma exceçãoJsonMappingException:

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

Ofull exception é:

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

Vamos ver, ao longo das próximas seções - como resolver esse problema.

3. Use@JsonManagedReference,@JsonBackReference

Primeiro, vamos anotar a relação com@JsonManagedReference,@JsonBackReference para permitir que Jackson lide melhor com a relação:

Aqui está a entidade “User”:

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

    @JsonBackReference
    public List userItems;
}

E o “Item“:

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

    @JsonManagedReference
    public User owner;
}

Vamos agora testar as novas entidades:

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

Aqui está a saída da serialização:

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

Observe que:

  • @JsonManagedReference é a parte direta da referência - aquela que é serializada normalmente.

  • @JsonBackReference é a parte posterior da referência - será omitido da serialização.

4. Use@JsonIdentityInfo

Agora - vamos ver como ajudar na serialização de entidades com relacionamento bidirecional usando@JsonIdentityInfo.

Adicionamos a anotação de nível de classe à nossa entidade “User”:

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

E para a entidade “Item”:

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

Hora do teste:

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

Aqui está a saída da serialização:

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

5. Use@JsonIgnore

Alternativamente, também podemos usar a anotação@JsonIgnore para simplesmenteignore one of the sides of the relationship, quebrando assim a cadeia.

No exemplo a seguir - evitaremos a recursão infinita, ignorando a propriedade “User” “userItems” da serialização:

Aqui está a entidade “User”:

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

    @JsonIgnore
    public List userItems;
}

E aqui está o nosso teste:

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

E aqui está a saída da serialização:

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

6. Use@JsonView

Também podemos usar a anotação@JsonView mais recente para excluir um lado do relacionamento.

No exemplo a seguir - usamostwo JSON Views – Public and Internal, ondeInternal estendePublic:

public class Views {
    public static class Public {}

    public static class Internal extends Public {}
}

Incluiremos todos os camposUser eItem na visualizaçãoPublic -except the User field userItems que serão incluídos na visualizaçãoInternal:

Aqui está nossa entidade “User“:

public class User {
    @JsonView(Views.Public.class)
    public int id;

    @JsonView(Views.Public.class)
    public String name;

    @JsonView(Views.Internal.class)
    public List userItems;
}

E aqui está nossa entidade “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;
}

Quando serializamos usando a visualizaçãoPublic, funciona corretamente -because we excluded userItems de ser serializado:

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

Mas se serializarmos usando uma visualizaçãoInternal,JsonMappingException é lançado porque todos os campos estão incluídos:

@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 um serializador personalizado

A seguir - vamos ver como serializar entidades com relacionamento bidirecional usando um serializador personalizado.

No exemplo a seguir - usaremos um serializador personalizado para serializar a propriedade “User” “userItems“:

Aqui está a entidade “User”:

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

    @JsonSerialize(using = CustomListSerializer.class)
    public List userItems;
}

E aqui está o “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);
    }
}

Vamos agora testar o serializador e ver o tipo certo de saída sendo produzida:

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

Ethe final output da serialização com o serializador personalizado:

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

8. Desserializar com@JsonIdentityInfo

Agora - vamos ver como desserializar entidades com relacionamento bidirecional usando@JsonIdentityInfo.

Aqui está a entidade “User”:

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

E a entidade “Item”:

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

Vamos agora escrever um teste rápido - começando com alguns dados JSON manuais que queremos analisar e terminando com a entidade construída corretamente:

@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. Usar desserializador personalizado

Finalmente, vamos desserializar as entidades com relacionamento bidirecional usando um desserializador personalizado.

No exemplo a seguir - usaremos o desserializador personalizado para analisar a propriedade “User” “userItems“:

Aqui está a entidade “User”:

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

    @JsonDeserialize(using = CustomListDeserializer.class)
    public List userItems;
}

E aqui está o nosso “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<>();
    }
}

E o teste simples:

@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. Conclusão

Neste tutorial, ilustramos como serializar / desserializar entidades com relacionamentos bidirecionais usando Jackson.

A implementação de todos esses exemplos e trechos de códigocan be found in our GitHub project - este é um projeto baseado em Maven, portanto, deve ser fácil de importar e executar como está.