Mais anotações de Jackson

Mais anotações de Jackson

1. Visão geral

Este artigo cobre algumas anotações adicionais que não foram abordadas no artigo anterior,A Guide to Jackson Annotations - examinaremos sete delas.

2. @JsonIdentityReference

@JsonIdentityReference é usado para customização de referências a objetos que serão serializados como identidades de objeto em vez de POJOs completos. Ele funciona em colaboração com@JsonIdentityInfo para forçar o uso de identidades de objeto em cada serialização, diferente de todos, exceto na primeira vez, quando@JsonIdentityReference está ausente. Essas anotações são mais úteis ao lidar com dependências circulares entre objetos. Consulte a seção 4 do artigoJackson – Bidirectional Relationship para obter mais informações.

Para demonstrar o uso de@JsonIdentityReference, definiremos duas classes de bean diferentes, sem e com esta anotação.

O feijão sem@JsonIdentityReference:

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

    // constructor, getters and setters
}

Para o bean usando@JsonIdentityReference, escolhemos a propriedadeid para ser a identidade do objeto:

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

    // constructor, getters and setters
}

No primeiro caso, onde@JsonIdentityReference está ausente, esse bean é serializado com todos os detalhes de suas propriedades:

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

A saída da serialização acima:

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

Quando@JsonIdentityReference é usado, o bean é serializado como uma identidade simples ao invés:

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

3. @JsonAppend

A anotação@JsonAppend é usada para adicionar propriedades virtuais a um objeto, além das normais, quando o objeto é serializado. Isso é necessário quando queremos adicionar informações suplementares diretamente a uma string JSON, em vez de alterar a definição de classe. Por exemplo, pode ser mais conveniente inserir os metadadosversion de um bean no documento JSON correspondente do que fornecer a ele uma propriedade adicional.

Suponha que temos um bean sem@JsonAppend da seguinte forma:

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

    // constructor, getters and setters
}

Um teste irá confirmar que, na ausência da anotação@JsonAppend, a saída de serialização não contém informações sobre a propriedadeversion suplementar, apesar do fato de tentarmos adicionar ao objetoObjectWriter :

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

A saída de serialização:

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

Agora, digamos que temos um bean anotado com@JsonAppend:

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

    // constructor, getters and setters
}

Um teste semelhante ao anterior verificará que quando a anotação@JsonAppend é aplicada, a propriedade suplementar é incluída após a serialização:

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

A saída dessa serialização mostra que a propriedadeversion foi adicionada:

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

4. @JsonNaming

A anotação@JsonNaming é usada para escolher as estratégias de nomenclatura para propriedades em serialização, substituindo o padrão. Usando o elementovalue, podemos especificar qualquer estratégia, incluindo as personalizadas.

Além do padrão, que éLOWER_CAMEL_CASE (por exemplo lowerCamelCase), a biblioteca Jackson fornece quatro outras estratégias de nomenclatura de propriedade integradas para sua conveniência:

  • KEBAB_CASE: os elementos de nome são separados por hifens, por exemplo kebab-case.

  • LOWER_CASE: todas as letras são minúsculas sem separadores, por exemplo, lowercase.

  • SNAKE_CASE: todas as letras são minúsculas com sublinhados como separadores entre elementos de nome, por exemplo, snake_case.

  • UPPER_CAMEL_CASE: todos os elementos de nome, incluindo o primeiro, começam com uma letra maiúscula, seguida por letras minúsculas e não há separadores, por exemplo, UpperCamelCase.

Este exemplo ilustrará a maneira de serializar propriedades usando nomes de caso de cobra, onde uma propriedade chamadabeanName é serializada comobean_name.

Dada uma definição de bean:

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

    // constructor, getters and setters
}

O teste abaixo demonstra que a regra de nomenclatura especificada funciona conforme necessário:

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

A variáveljsonString contém os seguintes dados:

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

5. @JsonPropertyDescription

A biblioteca Jackson é capaz de criar esquemas JSON para tipos Java com a ajuda de um módulo separado chamadoJSON Schema. O esquema é útil quando queremos especificar a saída esperada ao serializar objetos Java ou validar um documento JSON antes da desserialização.

A anotação@JsonPropertyDescription permite que uma descrição legível por humanos seja adicionada ao esquema JSON criado, fornecendo o campodescription.

Esta seção usa o bean declarado abaixo para demonstrar os recursos de@JsonPropertyDescription:

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

    // getters and setters
}

O método para gerar um esquema JSON com a adição do campodescription é mostrado abaixo:

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

Como podemos ver, a geração do esquema JSON foi bem-sucedida:

{
    "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

A anotação@JsonPOJOBuilder é usada para configurar uma classe de construtor para customizar a desserialização de um documento JSON para recuperar POJOs quando a convenção de nomenclatura é diferente do padrão.

Suponha que precisamos desserializar a seguinte string JSON:

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

Essa fonte JSON será usada para criar uma instância doPOJOBuilderBean:

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

    // constructor, getters and setters
}

Os nomes das propriedades do bean são diferentes daqueles dos campos na string JSON. É aqui que@JsonPOJOBuilder vem ao resgate.

A anotação@JsonPOJOBuilder é acompanhada por duas propriedades:

  • buildMethodName: o nome do método sem arg usado para instanciar o bean esperado após vincular campos JSON às propriedades desse bean. O nome padrão ébuild.

  • withPrefix: o prefixo do nome para a detecção automática de correspondência entre o JSON e as propriedades do bean. O prefixo padrão éwith.

Este exemplo usa a classeBeanBuilder abaixo, que é usada emPOJOBuilderBean:

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

No código acima, configuramos@JsonPOJOBuilder para usar um método de construção chamadocreateBeane o prefixoconstruct para propriedades correspondentes.

A aplicação de@JsonPOJOBuilder a um feijão é descrita e testada da seguinte forma:

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());

O resultado mostra que um novo objeto de dados foi recriado com êxito a partir de uma origem JSON, apesar de uma incompatibilidade nos nomes das propriedades.

7. @JsonTypeId

A anotação@JsonTypeId é usada para indicar que a propriedade anotada deve ser serializada como o id de tipo ao incluir informações de tipo polimórfico, em vez de uma propriedade regular. Esses metadados polimórficos são usados ​​durante a desserialização para recriar objetos dos mesmos subtipos que eram antes da serialização, em vez dos supertipos declarados.

Para obter mais informações sobre o tratamento de herança de Jackson, consulte a seção 2 doInheritance in Jackson.

Digamos que temos uma definição de classe de bean da seguinte maneira:

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

    // constructor, getters and setters
}

O teste a seguir valida que@JsonTypeId funciona como deveria:

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

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

Saída do processo de serialização:

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

8. @JsonTypeIdResolver

A anotação@JsonTypeIdResolver é usada para significar um manipulador de identidade de tipo customizado na serialização e desserialização. Esse manipulador é responsável pela conversão entre os tipos Java e o ID do tipo incluído em um documento JSON.

Suponha que desejamos incorporar informações de tipo em uma string JSON ao lidar com a seguinte hierarquia de classes.

A 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
}

A subclasseFirstBean:

public class FirstBean extends AbstractBean {
    String firstName;

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

    // no-arg constructor, getter and setter
}

A subclasseLastBean:

public class LastBean extends AbstractBean {
    String lastName;

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

    // no-arg constructor, getter and setter
}

As instâncias dessas classes são usadas para preencher um objetoBeanContainer:

public class BeanContainer {
    private List beans;

    // getter and setter
}

Podemos ver que a classeAbstractBean é anotada com@JsonTypeIdResolver, indicando que ela usa umTypeIdResolver personalizado para decidir como incluir informações de subtipo na serialização e como usar esses metadados do outro caminho de volta.

Aqui está a classe resolvedor para lidar com a inclusão de informações de tipo:

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

Os dois métodos mais notáveis ​​sãoidFromValueAndTypeetypeFromId, com o primeiro informando a maneira de incluir informações de tipo ao serializar POJOs e o último determinando os subtipos de objetos recriados usando esses metadados.

Para garantir que a serialização e a desserialização funcionem bem, vamos escrever um teste para validar o progresso completo.

Primeiro, precisamos instanciar um contêiner de bean e classes de bean e, em seguida, preencher esse contêiner com instâncias de bean:

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

Em seguida, o objetoBeanContainer é serializado e confirmamos que a string resultante contém informações de tipo:

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

A saída da serialização é mostrada abaixo:

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

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

Essa estrutura JSON será usada para recriar objetos dos mesmos subtipos de antes da serialização. Aqui estão as etapas de implementação para desserialização:

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

Este tutorial explicou várias anotações menos comuns de Jackson em detalhes. A implementação desses exemplos e trechos de código pode ser encontrada ema GitHub project.