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á.