Jackson - Relations bidirectionnelles

Jackson - Relations bidirectionnelles

1. Vue d'ensemble

Dans ce didacticiel, nous allons passer en revue les meilleures façons de gérer lesbidirectional relationships in Jackson.

Nous discuterons du problème de récursion 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, examinons le problème de récursivité infinie de Jackson. Dans l'exemple suivant, nous avons deux entités - «User» et «Item» - aveca simple one-to-many relationship:

L'entité «User»:

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

L'entité «Item»:

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 exceptionJsonMappingException:

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

Lefull exception est:

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

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é "User":

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

    @JsonBackReference
    public List userItems;
}

Et les «Item»:

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

    @JsonManagedReference
    public User owner;
}

Essayons 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é «User»:

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

Et à l'entité «Item»:

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

Temps pour le 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 simplementignore one of the sides of the relationship, 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é «User»:

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 utilisonstwo JSON Views – Public and InternalInternal étendPublic:

public class Views {
    public static class Public {}

    public static class Internal extends Public {}
}

Nous inclurons tous les champsUser etItem dans la vuePublic -except the User field userItems qui seront inclus dans la vueInternal:

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

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 en utilisant la vuePublic, cela fonctionne correctement -because we excluded userItems d'être sérialisé:

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

Mais si nous sérialisons en utilisant une vueInternal,JsonMappingException est renvoyé car tous les champs sont inclus:

@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. Utiliser un sérialiseur personnalisé

Ensuite, voyons comment sérialiser des entités avec une relation bidirectionnelle à l'aide d'un sérialiseur personnalisé.

Dans l'exemple suivant - nous utiliserons un sérialiseur personnalisé pour sérialiser la propriété «User» «userItems»:

Voici l’entité "User":

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

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

Et voici les «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);
    }
}

Testons maintenant le sérialiseur et voyons le bon type de sortie produit:

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

Etthe final output de la sérialisation avec le sérialiseur personnalisé:

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

8. Désérialiser avec@JsonIdentityInfo

Voyons maintenant comment désérialiser des entités avec une relation bidirectionnelle à l'aide de@JsonIdentityInfo.

Voici l'entité «User»:

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

Et l'entité «Item»:

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

Écrivons maintenant un test rapide - en commençant par quelques données JSON manuelles que nous voulons analyser et en terminant par l'entité correctement construite:

@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. Utiliser le désérialiseur personnalisé

Enfin, désérialisons les entités avec une relation bidirectionnelle à l'aide d'un désérialiseur personnalisé.

Dans l'exemple suivant - nous utiliserons le désérialiseur personnalisé pour analyser la propriété «User» «userItems»:

Voici l'entité «User»:

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

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

Et voici nos «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<>();
    }
}

Et le test simple:

@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 codecan be found in our GitHub project - il s'agit d'un projet basé sur Maven, il devrait donc être facile à importer et à exécuter tel quel.