Altre annotazioni di Jackson

1. Panoramica

Questo articolo copre alcune annotazioni aggiuntive che non sono state trattate nell'articolo precedente, A Guide to Jackson Annotations - ne esamineremo sette.

2. @JsonIdentityReference

@JsonIdentityReference viene utilizzato per la personalizzazione dei riferimenti agli oggetti che verranno serializzati come identità degli oggetti anziché come POJO completi. Funziona in collaborazione con @JsonIdentityInfo per forzare l'utilizzo delle identità degli oggetti in ogni serializzazione, in modo diverso da quando @JsonIdentityReference è assente. Questa coppia di annotazioni è molto utile quando si tratta di dipendenze circolari tra oggetti. Fare riferimento alla sezione 4 dell'articolo Jackson - Relazione bidirezionale per ulteriori informazioni.

Per dimostrare l'uso di @JsonIdentityReference , definiremo due diverse classi di bean, senza e con questa annotazione.

Il fagiolo senza @JsonIdentityReference :

@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") public class BeanWithoutIdentityReference { private int id; private String name; // constructor, getters and setters }

Per il bean che utilizza @JsonIdentityReference , scegliamo la proprietà id come identità dell'oggetto:

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

Nel primo caso, dove @JsonIdentityReference è assente, quel bean viene serializzato con tutti i dettagli sulle sue proprietà:

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

L'output della serializzazione sopra:

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

Quando viene utilizzato @JsonIdentityReference , il bean viene invece serializzato come un'identità semplice:

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

3. @JsonAppend

L' annotazione @JsonAppend viene utilizzata per aggiungere proprietà virtuali a un oggetto oltre a quelle normali quando tale oggetto viene serializzato. Ciò è necessario quando vogliamo aggiungere informazioni supplementari direttamente in una stringa JSON, piuttosto che modificare la definizione della classe. Ad esempio, potrebbe essere più conveniente inserire i metadati della versione di un bean nel documento JSON corrispondente piuttosto che fornirgli una proprietà aggiuntiva.

Supponiamo di avere un bean senza @JsonAppend come segue:

public class BeanWithoutAppend { private int id; private String name; // constructor, getters and setters }

Un test confermerà che in assenza dell'annotazione @JsonAppend , l'output della serializzazione non contiene informazioni sulla proprietà della versione supplementare , nonostante si tenti di aggiungere all'oggetto ObjectWriter :

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

L'output di serializzazione:

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

Ora, supponiamo di avere un bean annotato con @JsonAppend :

@JsonAppend(attrs = { @JsonAppend.Attr(value = "version") }) public class BeanWithAppend { private int id; private String name; // constructor, getters and setters }

Un test simile al precedente verificherà che quando viene applicata l'annotazione @JsonAppend , la proprietà supplementare viene inclusa dopo la serializzazione:

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

L'output di tale serializzazione mostra che la proprietà version è stata aggiunta:

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

4. @JsonNaming

L' annotazione @JsonNaming viene utilizzata per scegliere le strategie di denominazione per le proprietà nella serializzazione, sovrascrivendo l'impostazione predefinita. Utilizzando l' elemento value , possiamo specificare qualsiasi strategia, comprese quelle personalizzate.

Oltre all'impostazione predefinita, che è LOWER_CAMEL_CASE (ad esempio lowerCamelCase ), la libreria Jackson ci fornisce altre quattro strategie di denominazione delle proprietà integrate per comodità:

  • KEBAB_CASE : gli elementi del nome sono separati da trattini, ad esempio kebab-case .
  • LOWER_CASE : tutte le lettere sono minuscole senza separatori, ad esempio minuscole .
  • SNAKE_CASE : tutte le lettere sono minuscole con trattini bassi come separatori tra gli elementi del nome, ad esempio snake_case .
  • UPPER_CAMEL_CASE : Tutti gli elementi del nome, incluso il primo, iniziano con una lettera maiuscola, seguita da lettere minuscole e non ci sono separatori, ad esempio UpperCamelCase .

Questo esempio illustrerà il modo per serializzare le proprietà utilizzando i nomi dei casi di serpente, dove una proprietà denominata beanName viene serializzata come bean_name.

Data una definizione di fagiolo:

@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) public class NamingBean { private int id; private String beanName; // constructor, getters and setters }

Il test seguente dimostra che la regola di denominazione specificata funziona come richiesto:

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

La variabile jsonString contiene i seguenti dati:

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

5. @JsonPropertyDescription

La libreria Jackson è in grado di creare schemi JSON per i tipi Java con l'aiuto di un modulo separato chiamato JSON Schema. Lo schema è utile quando si desidera specificare l'output previsto durante la serializzazione di oggetti Java o per convalidare un documento JSON prima della deserializzazione.

L' annotazione @JsonPropertyDescription consente di aggiungere una descrizione leggibile dall'uomo allo schema JSON creato fornendo il campo della descrizione .

Questa sezione fa uso del bean dichiarato di seguito per dimostrare le capacità di @JsonPropertyDescription :

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

Di seguito è mostrato il metodo per generare uno schema JSON con l'aggiunta del campo descrizione :

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

Come possiamo vedere, la generazione dello schema JSON ha avuto successo:

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

6. @JsonPOJOBuilder

L' annotazione @JsonPOJOBuilder viene utilizzata per configurare una classe builder per personalizzare la deserializzazione di un documento JSON per recuperare POJO quando la convenzione di denominazione è diversa da quella predefinita.

Supponiamo di dover deserializzare la seguente stringa JSON:

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

Quella sorgente JSON verrà utilizzata per creare un'istanza di POJOBuilderBean :

@JsonDeserialize(builder = BeanBuilder.class) public class POJOBuilderBean { private int identity; private String beanName; // constructor, getters and setters }

I nomi delle proprietà del bean sono diversi da quelli dei campi nella stringa JSON. È qui che @JsonPOJOBuilder viene in soccorso.

L' annotazione @JsonPOJOBuilder è accompagnata da due proprietà:

  • buildMethodName : il nome del metodo no-arg utilizzato per istanziare il bean previsto dopo aver associato i campi JSON alle proprietà di quel bean. Il nome predefinito è build .
  • withPrefix : il prefisso del nome per il rilevamento automatico della corrispondenza tra JSON e le proprietà del bean. Il prefisso predefinito è con .

Questo esempio fa uso della classe BeanBuilder di seguito, che viene utilizzata su POJOBuilderBean :

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

Nel codice precedente, abbiamo configurato @JsonPOJOBuilder per utilizzare un metodo di compilazione chiamato createBean e il prefisso del costrutto per le proprietà corrispondenti.

L'applicazione di @JsonPOJOBuilder a un bean viene descritta e testata come segue:

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

Il risultato mostra che un nuovo oggetto dati è stato ricreato con successo da un'origine JSON nonostante una mancata corrispondenza nei nomi delle proprietà.

7. @JsonTypeId

L' annotazione @JsonTypeId viene utilizzata per indicare che la proprietà annotata deve essere serializzata come id di tipo quando si includono informazioni sul tipo polimorfico, piuttosto che come proprietà regolare. Tali metadati polimorfici vengono utilizzati durante la deserializzazione per ricreare oggetti degli stessi sottotipi di prima della serializzazione, anziché dei supertipi dichiarati.

Per ulteriori informazioni sulla gestione dell'eredità da parte di Jackson, vedere la sezione 2 di Inheritance in Jackson.

Supponiamo di avere una definizione di classe bean come segue:

public class TypeIdBean { private int id; @JsonTypeId private String name; // constructor, getters and setters }

The following test validates that @JsonTypeId works as it is meant to:

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

The serialization process' output:

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

8. @JsonTypeIdResolver

The @JsonTypeIdResolver annotation is used to signify a custom type identity handler in serialization and deserialization. That handler is responsible for conversion between Java types and type id included in a JSON document.

Suppose that we want to embed type information in a JSON string when dealing with the following class hierarchy.

The AbstractBean superclass:

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

The FirstBean subclass:

public class FirstBean extends AbstractBean { String firstName; public FirstBean(int id, String name) { super(id); setFirstName(name); } // no-arg constructor, getter and setter }

The LastBean subclass:

public class LastBean extends AbstractBean { String lastName; public LastBean(int id, String name) { super(id); setLastName(name); } // no-arg constructor, getter and setter }

Instances of those classes are used to populate a BeanContainer object:

public class BeanContainer { private List beans; // getter and setter }

We can see that the AbstractBean class is annotated with @JsonTypeIdResolver, indicating that it uses a custom TypeIdResolver to decide how to include subtype information in serialization and how to make use of that metadata the other way round.

Here is the resolver class to handle inclusion of type information:

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

The two most notable methods are idFromValueAndType and typeFromId, with the former telling the way to include type information when serializing POJOs and the latter determining the subtypes of re-created objects using that metadata.

In order to make sure that both serialization and deserialization work well, let's write a test to validate the complete progress.

Innanzitutto, dobbiamo creare un'istanza di un contenitore di bean e delle classi di bean, quindi popolare quel contenitore con istanze di 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);

Successivamente, l' oggetto BeanContainer viene serializzato e confermiamo che la stringa risultante contiene informazioni sul tipo:

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

L'output della serializzazione è mostrato di seguito:

{ "beans": [ { "@type": "bean1", "id": 1, "firstName": "Bean 1" }, { "@type": "bean2", "id": 2, "lastName": "Bean 2" } ] }

La struttura JSON verrà utilizzata per ricreare oggetti degli stessi sottotipi di prima della serializzazione. Ecco i passaggi di implementazione per la deserializzazione:

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

Questo tutorial ha spiegato in dettaglio diverse annotazioni di Jackson meno comuni. L'implementazione di questi esempi e frammenti di codice può essere trovata in un progetto GitHub.