Jackson - Relations bidirectionnelles

1. Vue d'ensemble

Dans ce didacticiel, nous allons passer en revue les meilleures façons de gérer les relations bidirectionnelles dans Jackson .

Nous discuterons du problème de récursivité infinie Jackson JSON, puis - nous verrons comment sérialiser des entités avec des relations bidirectionnelles et enfin - nous les désérialiserons.

2. Récursivité infinie

Tout d'abord, jetons un coup d'œil au problème de récursivité infinie de Jackson. Dans l'exemple suivant, nous avons deux entités - « Utilisateur » et « Élément » - avec une simple relation un-à-plusieurs :

L' entité « Utilisateur »:

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

L' entité « Article »:

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

Lorsque nous essayons de sérialiser une instance de " Item ", Jackson lèvera une exception 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); }

L' exception complète est:

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"] ->…..

Voyons, au cours des prochaines sections, comment résoudre ce problème.

3. Utilisez @JsonManagedReference , @JsonBackReference

Tout d'abord, annotons la relation avec @JsonManagedReference , @JsonBackReference pour permettre à Jackson de mieux gérer la relation:

Voici l' entité " Utilisateur ":

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

Et le " Item ":

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

Testons maintenant les nouvelles entités:

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

Voici la sortie de la sérialisation:

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

Notez que:

  • @JsonManagedReference est la partie avant de la référence - celle qui est sérialisée normalement.
  • @JsonBackReference est la partie arrière de la référence - elle sera omise de la sérialisation.

4. Utilisez @JsonIdentityInfo

Voyons maintenant comment aider à la sérialisation d'entités avec une relation bidirectionnelle à l'aide de @JsonIdentityInfo .

Nous ajoutons l'annotation au niveau de la classe à notre entité « Utilisateur »:

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

Et à l' entité " Item ":

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

Heure du test:

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

Voici la sortie de la sérialisation:

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

5. Utilisez @JsonIgnore

Alternativement, nous pouvons également utiliser l' annotation @JsonIgnore pour simplement ignorer l'un des côtés de la relation , rompant ainsi la chaîne.

Dans l'exemple suivant, nous éviterons la récursivité infinie en ignorant la propriété « User » « userItems » de la sérialisation:

Voici l' entité « Utilisateur »:

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

Et voici notre test:

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

Et voici la sortie de la sérialisation:

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

6. Utilisez @JsonView

Nous pouvons également utiliser la nouvelle annotation @JsonView pour exclure un côté de la relation.

Dans l'exemple suivant, nous utilisons deux vues JSON - Public et InternalInternal étend Public :

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

Nous inclurons tous les champs Utilisateur et Élément dans la vue publique - à l'exception du champ Utilisateur userItems qui sera inclus dans la vue interne :

Voici notre entité « Utilisateur »:

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

Et voici notre entité « 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; }

Lorsque nous sérialisons à l'aide de la vue publique , cela fonctionne correctement - car nous avons exclu userItems de la sérialisation:

@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

Dans ce didacticiel, nous avons illustré comment sérialiser / désérialiser des entités avec des relations bidirectionnelles à l'aide de Jackson.

L'implémentation de tous ces exemples et extraits de code peut être trouvée dans notre projet GitHub - il s'agit d'un projet basé sur Maven, il devrait donc être facile à importer et à exécuter tel quel.