Plus d’annotations de Jackson

Plus d'annotations de Jackson

1. Vue d'ensemble

Cet article couvre certaines annotations supplémentaires qui n'étaient pas couvertes dans l'article précédent,A Guide to Jackson Annotations - nous en passerons en revue sept.

2. @JsonIdentityReference

@JsonIdentityReference est utilisé pour la personnalisation des références aux objets qui seront sérialisés en tant qu'identités d'objet au lieu de POJO complets. Il fonctionne en collaboration avec@JsonIdentityInfo pour forcer l'utilisation des identités d'objet dans chaque sérialisation, différent de tout sauf la première fois lorsque@JsonIdentityReference est absent. Ce couple d’annotations est particulièrement utile lorsqu’il s’agit de dépendances circulaires entre objets. Veuillez vous référer à la section 4 de l'article deJackson – Bidirectional Relationship pour plus d'informations.

Afin de démontrer l'utilisation de@JsonIdentityReference, nous allons définir deux classes de bean différentes, sans et avec cette annotation.

Le bean sans@JsonIdentityReference:

@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class BeanWithoutIdentityReference {
    private int id;
    private String name;

    // constructor, getters and setters
}

Pour le bean utilisant@JsonIdentityReference, nous choisissons la propriétéid comme identité de l'objet:

@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
@JsonIdentityReference(alwaysAsId = true)
public class BeanWithIdentityReference {
    private int id;
    private String name;

    // constructor, getters and setters
}

Dans le premier cas, où@JsonIdentityReference est absent, ce bean est sérialisé avec tous les détails sur ses propriétés:

BeanWithoutIdentityReference bean
  = new BeanWithoutIdentityReference(1, "Bean Without Identity Reference Annotation");
String jsonString = mapper.writeValueAsString(bean);

La sortie de la sérialisation ci-dessus:

{
    "id": 1,
    "name": "Bean Without Identity Reference Annotation"
}

Lorsque@JsonIdentityReference est utilisé, le bean est sérialisé comme une identité simple à la place:

BeanWithIdentityReference bean
  = new BeanWithIdentityReference(1, "Bean With Identity Reference Annotation");
String jsonString = mapper.writeValueAsString(bean);
assertEquals("1", jsonString);

3. @JsonAppend

L'annotation@JsonAppend est utilisée pour ajouter des propriétés virtuelles à un objet en plus des propriétés normales lorsque cet objet est sérialisé. Cela est nécessaire lorsque nous voulons ajouter des informations supplémentaires directement dans une chaîne JSON, plutôt que de modifier la définition de la classe. Par exemple, il peut être plus pratique d'insérer les métadonnéesversion d'un bean dans le document JSON correspondant que de lui fournir une propriété supplémentaire.

Supposons que nous ayons un bean sans@JsonAppend comme suit:

public class BeanWithoutAppend {
    private int id;
    private String name;

    // constructor, getters and setters
}

Un test confirmera qu'en l'absence de l'annotation@JsonAppend, la sortie de sérialisation ne contient pas d'informations sur la propriété supplémentaireversion, malgré le fait que nous essayons d'ajouter à l'objetObjectWriter :

BeanWithoutAppend bean = new BeanWithoutAppend(2, "Bean Without Append Annotation");
ObjectWriter writer
  = mapper.writerFor(BeanWithoutAppend.class).withAttribute("version", "1.0");
String jsonString = writer.writeValueAsString(bean);

La sortie de sérialisation:

{
    "id": 2,
    "name": "Bean Without Append Annotation"
}

Maintenant, disons que nous avons un bean annoté avec@JsonAppend:

@JsonAppend(attrs = {
  @JsonAppend.Attr(value = "version")
})
public class BeanWithAppend {
    private int id;
    private String name;

    // constructor, getters and setters
}

Un test similaire au précédent vérifiera que lorsque l'annotation@JsonAppend est appliquée, la propriété supplémentaire est incluse après la sérialisation:

BeanWithAppend bean = new BeanWithAppend(2, "Bean With Append Annotation");
ObjectWriter writer
  = mapper.writerFor(BeanWithAppend.class).withAttribute("version", "1.0");
String jsonString = writer.writeValueAsString(bean);

La sortie de cette sérialisation montre que la propriétéversion a été ajoutée:

{
    "id": 2,
    "name": "Bean With Append Annotation",
    "version": "1.0"
}

4. @JsonNaming

L'annotation@JsonNaming est utilisée pour choisir les stratégies de dénomination des propriétés en sérialisation, en remplaçant la valeur par défaut. En utilisant l'élémentvalue, nous pouvons spécifier n'importe quelle stratégie, y compris les stratégies personnalisées.

En plus de la valeur par défaut, qui estLOWER_CAMEL_CASE (par ex. lowerCamelCase), la bibliothèque Jackson nous fournit quatre autres stratégies de dénomination de propriété intégrées pour plus de commodité:

  • KEBAB_CASE: les éléments de nom sont séparés par des tirets, par ex. kebab-case.

  • LOWER_CASE: toutes les lettres sont en minuscules sans séparateurs, par ex. lowercase.

  • SNAKE_CASE: toutes les lettres sont en minuscules avec des traits de soulignement comme séparateurs entre les éléments du nom, par exemple snake_case.

  • UPPER_CAMEL_CASE: Tous les éléments de nom, y compris le premier, commencent par une lettre majuscule, suivie de minuscules et il n'y a pas de séparateurs, par exemple UpperCamelCase.

Cet exemple illustrera la manière de sérialiser des propriétés à l'aide de noms de cas de serpent, où une propriété nomméebeanName est sérialisée en tant quebean_name.

Étant donné une définition de haricot:

@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
public class NamingBean {
    private int id;
    private String beanName;

    // constructor, getters and setters
}

Le test ci-dessous montre que la règle de dénomination spécifiée fonctionne comme prévu:

NamingBean bean = new NamingBean(3, "Naming Bean");
String jsonString = mapper.writeValueAsString(bean);
assertThat(jsonString, containsString("bean_name"));

La variablejsonString contient les données suivantes:

{
    "id": 3,
    "bean_name": "Naming Bean"
}

5. @JsonPropertyDescription

La bibliothèque Jackson est capable de créer des schémas JSON pour les types Java à l'aide d'un module séparé appeléJSON Schema. Le schéma est utile lorsque vous souhaitez spécifier la sortie attendue lors de la sérialisation d'objets Java ou valider un document JSON avant la désérialisation.

L'annotation@JsonPropertyDescription permet d'ajouter une description lisible par l'homme au schéma JSON créé en fournissant le champdescription.

Cette section utilise le bean déclaré ci-dessous pour démontrer les capacités de@JsonPropertyDescription:

public class PropertyDescriptionBean {
    private int id;
    @JsonPropertyDescription("This is a description of the name property")
    private String name;

    // getters and setters
}

La méthode de génération d'un schéma JSON avec l'ajout du champdescription est illustrée ci-dessous:

SchemaFactoryWrapper wrapper = new SchemaFactoryWrapper();
mapper.acceptJsonFormatVisitor(PropertyDescriptionBean.class, wrapper);
JsonSchema jsonSchema = wrapper.finalSchema();
String jsonString = mapper.writeValueAsString(jsonSchema);
assertThat(jsonString, containsString("This is a description of the name property"));

Comme nous pouvons le constater, la génération du schéma JSON a réussi:

{
    "type": "object",
    "id": "urn:jsonschema:com:example:jackson:annotation:extra:PropertyDescriptionBean",
    "properties":
    {
        "name":
        {
            "type": "string",
            "description": "This is a description of the name property"
        },

        "id":
        {
            "type": "integer"
        }
    }
}

6. @JsonPOJOBuilder

L'annotation@JsonPOJOBuilder est utilisée pour configurer une classe de générateur pour personnaliser la désérialisation d'un document JSON pour récupérer les POJO lorsque la convention de dénomination est différente de la convention de dénomination par défaut.

Supposons que nous ayons besoin de désérialiser la chaîne JSON suivante:

{
    "id": 5,
    "name": "POJO Builder Bean"
}

Cette source JSON sera utilisée pour créer une instance desPOJOBuilderBean:

@JsonDeserialize(builder = BeanBuilder.class)
public class POJOBuilderBean {
    private int identity;
    private String beanName;

    // constructor, getters and setters
}

Les noms des propriétés du bean sont différents de ceux des champs de la chaîne JSON. C'est là que@JsonPOJOBuilder vient à la rescousse.

L'annotation@JsonPOJOBuilder est accompagnée de deux propriétés:

  • buildMethodName: nom de la méthode sans argument utilisée pour instancier le bean attendu après avoir lié les champs JSON aux propriétés de ce bean. Le nom par défaut estbuild.

  • withPrefix: le préfixe de nom pour la détection automatique de la correspondance entre le JSON et les propriétés du bean. Le préfixe par défaut estwith.

Cet exemple utilise la classeBeanBuilder ci-dessous, qui est utilisée surPOJOBuilderBean:

@JsonPOJOBuilder(buildMethodName = "createBean", withPrefix = "construct")
public class BeanBuilder {
    private int idValue;
    private String nameValue;

    public BeanBuilder constructId(int id) {
        idValue = id;
        return this;
    }

    public BeanBuilder constructName(String name) {
        nameValue = name;
        return this;
    }

    public POJOBuilderBean createBean() {
        return new POJOBuilderBean(idValue, nameValue);
    }
}

Dans le code ci-dessus, nous avons configuré les@JsonPOJOBuilder pour utiliser une méthode de construction appeléecreateBean et le préfixeconstruct pour les propriétés correspondantes.

L'application de@JsonPOJOBuilder à un bean est décrite et testée comme suit:

String jsonString = "{\"id\":5,\"name\":\"POJO Builder Bean\"}";
POJOBuilderBean bean = mapper.readValue(jsonString, POJOBuilderBean.class);

assertEquals(5, bean.getIdentity());
assertEquals("POJO Builder Bean", bean.getBeanName());

Le résultat montre qu'un nouvel objet de données a été recréé avec succès à partir d'une source JSON malgré une incompatibilité dans les noms des propriétés.

7. @JsonTypeId

L'annotation@JsonTypeId est utilisée pour indiquer que la propriété annotée doit être sérialisée en tant qu'ID de type lors de l'inclusion d'informations de type polymorphe, plutôt qu'en tant que propriété régulière. Ces métadonnées polymorphes sont utilisées lors de la désérialisation pour recréer des objets des mêmes sous-types qu’avant la sérialisation, plutôt que des supertypes déclarés.

Pour plus d’informations sur la gestion de l’héritage par Jackson, consultez la section 2 desInheritance in Jackson.

Disons que nous avons une définition de classe de bean comme suit:

public class TypeIdBean {
    private int id;
    @JsonTypeId
    private String name;

    // constructor, getters and setters
}

Le test suivant valide que@JsonTypeId fonctionne comme prévu:

mapper.enableDefaultTyping(DefaultTyping.NON_FINAL);
TypeIdBean bean = new TypeIdBean(6, "Type Id Bean");
String jsonString = mapper.writeValueAsString(bean);

assertThat(jsonString, containsString("Type Id Bean"));

La sortie du processus de sérialisation:

[
    "Type Id Bean",
    {
        "id": 6
    }
]

8. @JsonTypeIdResolver

L'annotation@JsonTypeIdResolver est utilisée pour désigner un gestionnaire d'identité de type personnalisé dans la sérialisation et la désérialisation. Ce gestionnaire est responsable de la conversion entre les types Java et le type id inclus dans un document JSON.

Supposons que nous voulions incorporer des informations de type dans une chaîne JSON lors de l'utilisation de la hiérarchie de classes suivante.

La superclasseAbstractBean:

@JsonTypeInfo(
  use = JsonTypeInfo.Id.NAME,
  include = JsonTypeInfo.As.PROPERTY,
  property = "@type"
)
@JsonTypeIdResolver(BeanIdResolver.class)
public class AbstractBean {
    private int id;

    protected AbstractBean(int id) {
        this.id = id;
    }

    // no-arg constructor, getter and setter
}

La sous-classeFirstBean:

public class FirstBean extends AbstractBean {
    String firstName;

    public FirstBean(int id, String name) {
        super(id);
        setFirstName(name);
    }

    // no-arg constructor, getter and setter
}

La sous-classeLastBean:

public class LastBean extends AbstractBean {
    String lastName;

    public LastBean(int id, String name) {
        super(id);
        setLastName(name);
    }

    // no-arg constructor, getter and setter
}

Les instances de ces classes sont utilisées pour remplir un objetBeanContainer:

public class BeanContainer {
    private List beans;

    // getter and setter
}

Nous pouvons voir que la classeAbstractBean est annotée avec@JsonTypeIdResolver, indiquant qu'elle utilise unTypeIdResolver personnalisé pour décider comment inclure les informations de sous-type dans la sérialisation et comment utiliser ces métadonnées l'autre inverse.

Voici la classe de résolveur pour gérer l'inclusion d'informations de type:

public class BeanIdResolver extends TypeIdResolverBase {

    private JavaType superType;

    @Override
    public void init(JavaType baseType) {
        superType = baseType;
    }

    @Override
    public Id getMechanism() {
        return Id.NAME;
    }

    @Override
    public String idFromValue(Object obj) {
        return idFromValueAndType(obj, obj.getClass());
    }

    @Override
    public String idFromValueAndType(Object obj, Class subType) {
        String typeId = null;
        switch (subType.getSimpleName()) {
        case "FirstBean":
            typeId = "bean1";
            break;
        case "LastBean":
            typeId = "bean2";
        }
        return typeId;
    }

    @Override
    public JavaType typeFromId(DatabindContext context, String id) {
        Class subType = null;
        switch (id) {
        case "bean1":
            subType = FirstBean.class;
            break;
        case "bean2":
            subType = LastBean.class;
        }
        return context.constructSpecializedType(superType, subType);
    }
}

Les deux méthodes les plus notables sontidFromValueAndType ettypeFromId, la première indiquant la manière d'inclure les informations de type lors de la sérialisation des POJO et la seconde déterminant les sous-types d'objets recréés à l'aide de ces métadonnées.

Afin de vous assurer que la sérialisation et la désérialisation fonctionnent correctement, écrivons un test pour valider la progression complète.

Tout d'abord, nous devons instancier un conteneur de beans et des classes de beans, puis renseigner ce conteneur avec des instances de beans:

FirstBean bean1 = new FirstBean(1, "Bean 1");
LastBean bean2 = new LastBean(2, "Bean 2");

List beans = new ArrayList<>();
beans.add(bean1);
beans.add(bean2);

BeanContainer serializedContainer = new BeanContainer();
serializedContainer.setBeans(beans);

Ensuite, l'objetBeanContainer est sérialisé et nous confirmons que la chaîne résultante contient des informations de type:

String jsonString = mapper.writeValueAsString(serializedContainer);
assertThat(jsonString, containsString("bean1"));
assertThat(jsonString, containsString("bean2"));

La sortie de la sérialisation est indiquée ci-dessous:

{
    "beans":
    [
        {
            "@type": "bean1",
            "id": 1,
            "firstName": "Bean 1"
        },

        {
            "@type": "bean2",
            "id": 2,
            "lastName": "Bean 2"
        }
    ]
}

Cette structure JSON sera utilisée pour recréer des objets des mêmes sous-types qu'avant la sérialisation. Voici les étapes de mise en œuvre pour la désérialisation:

BeanContainer deserializedContainer = mapper.readValue(jsonString, BeanContainer.class);
List beanList = deserializedContainer.getBeans();
assertThat(beanList.get(0), instanceOf(FirstBean.class));
assertThat(beanList.get(1), instanceOf(LastBean.class));

9. Conclusion

Ce tutoriel a expliqué en détail plusieurs annotations de Jackson moins courantes. L'implémentation de ces exemples et extraits de code se trouve dansa GitHub project.