Come mappare un valore nidificato a una proprietà utilizzando le annotazioni Jackson?


90

Supponiamo che stia effettuando una chiamata a un'API che risponde con il seguente JSON per un prodotto:

{
  "id": 123,
  "name": "The Best Product",
  "brand": {
     "id": 234,
     "name": "ACME Products"
  }
}

Sono in grado di mappare perfettamente l'ID e il nome del prodotto utilizzando le annotazioni di Jackson:

public class ProductTest {
    private int productId;
    private String productName, brandName;

    @JsonProperty("id")
    public int getProductId() {
        return productId;
    }

    public void setProductId(int productId) {
        this.productId = productId;
    }

    @JsonProperty("name")
    public String getProductName() {
        return productName;
    }

    public void setProductName(String productName) {
        this.productName = productName;
    }

    public String getBrandName() {
        return brandName;
    }

    public void setBrandName(String brandName) {
        this.brandName = brandName;
    }
}

E quindi utilizzando il metodo fromJson per creare il prodotto:

  JsonNode apiResponse = api.getResponse();
  Product product = Json.fromJson(apiResponse, Product.class);

Ma ora sto cercando di capire come afferrare il nome del marchio, che è una proprietà annidata. Speravo che qualcosa del genere funzionasse:

    @JsonProperty("brand.name")
    public String getBrandName() {
        return brandName;
    }

Ma ovviamente non lo fece. C'è un modo semplice per ottenere ciò che voglio usando le annotazioni?

La risposta JSON effettiva che sto cercando di analizzare è molto complessa e non voglio dover creare un'intera nuova classe per ogni sottonodo, anche se ho solo bisogno di un singolo campo.


2
Ho finito per usare github.com/json-path/JsonPath - Spring lo usa anche sotto il cofano. Ad esempio, nel loro org.springframework.data.web.
disumanizzatore

Risposte:


89

Puoi ottenere questo in questo modo:

String brandName;

@JsonProperty("brand")
private void unpackNameFromNestedObject(Map<String, String> brand) {
    brandName = brand.get("name");
}

23
Che ne dici di tre livelli di profondità?
Robin Kanters

5
@Robin Non funzionerebbe? this.abbreviation = ((Map<String, Object>)portalCharacteristics.get("icon")).get("ticker").toString();
Marcello de Sales

lo stesso metodo può essere utilizzato anche per decomprimere più valori ... unpackValuesFromNestedBrand - grazie
msanjay

13

Ecco come ho gestito questo problema:

Brand classe:

package org.answer.entity;

public class Brand {

    private Long id;

    private String name;

    public Brand() {

    }

    //accessors and mutators
}

Product classe:

package org.answer.entity;

import com.fasterxml.jackson.annotation.JsonGetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonSetter;

public class Product {

    private Long id;

    private String name;

    @JsonIgnore
    private Brand brand;

    private String brandName;

    public Product(){}

    @JsonGetter("brandName")
    protected String getBrandName() {
        if (brand != null)
            brandName = brand.getName();
        return brandName;
    }

    @JsonSetter("brandName")
    protected void setBrandName(String brandName) {
        if (brandName != null) {
            brand = new Brand();
            brand.setName(brandName);
        }
        this.brandName = brandName;
    }

//other accessors and mutators
}

Qui, l' brandistanza verrà ignorata da Jacksondurante serializatione deserialization, poiché è annotata con @JsonIgnore.

Jacksonuserà il metodo annotato con @JsonGetterfor serializationof java object in JSONformat. Quindi, brandNameè impostato con brand.getName().

Allo stesso modo, Jacksonutilizzerà il metodo annotato con @JsonSetterfor deserializationof JSONformat nell'oggetto java. In questo scenario, dovrai creare brandtu stesso un'istanza dell'oggetto e impostare la sua nameproprietà da brandName.

Puoi usare l' @Transientannotazione di persistenza con brandName, se vuoi che venga ignorata dal provider di persistenza.



1

La cosa migliore è usare i metodi setter:

JSON:

...
 "coordinates": {
               "lat": 34.018721,
               "lng": -118.489090
             }
...

Il metodo setter per lat o lng sarà simile a:

 @JsonProperty("coordinates")
    public void setLng(Map<String, String> coordinates) {
        this.lng = (Float.parseFloat(coordinates.get("lng")));
 }

se hai bisogno di leggere entrambi (come faresti normalmente), usa un metodo personalizzato

@JsonProperty("coordinates")
public void setLatLng(Map<String, String> coordinates){
    this.lat = (Float.parseFloat(coordinates.get("lat")));
    this.lng = (Float.parseFloat(coordinates.get("lng")));
}

-6

Per semplificare .. ho scritto il codice ... la maggior parte si spiega da sé.

Main Method

package com.test;

import java.io.IOException;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class LOGIC {

    public static void main(String[] args) throws JsonParseException, JsonMappingException, IOException {

        ObjectMapper objectMapper = new ObjectMapper();
        String DATA = "{\r\n" + 
                "  \"id\": 123,\r\n" + 
                "  \"name\": \"The Best Product\",\r\n" + 
                "  \"brand\": {\r\n" + 
                "     \"id\": 234,\r\n" + 
                "     \"name\": \"ACME Products\"\r\n" + 
                "  }\r\n" + 
                "}";

        ProductTest productTest = objectMapper.readValue(DATA, ProductTest.class);
        System.out.println(productTest.toString());

    }

}

Class ProductTest

package com.test;

import com.fasterxml.jackson.annotation.JsonProperty;

public class ProductTest {

    private int productId;
    private String productName;
    private BrandName brandName;

    @JsonProperty("id")
    public int getProductId() {
        return productId;
    }
    public void setProductId(int productId) {
        this.productId = productId;
    }

    @JsonProperty("name")
    public String getProductName() {
        return productName;
    }
    public void setProductName(String productName) {
        this.productName = productName;
    }

    @JsonProperty("brand")
    public BrandName getBrandName() {
        return brandName;
    }
    public void setBrandName(BrandName brandName) {
        this.brandName = brandName;
    }

    @Override
    public String toString() {
        return "ProductTest [productId=" + productId + ", productName=" + productName + ", brandName=" + brandName
                + "]";
    }



}

Class BrandName

package com.test;

public class BrandName {

    private Integer id;
    private String name;
    public Integer getId() {
        return id;
    }
    public void setId(Integer id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "BrandName [id=" + id + ", name=" + name + "]";
    }




}

OUTPUT

ProductTest [productId=123, productName=The Best Product, brandName=BrandName [id=234, name=ACME Products]]

6
Funziona, ma sto cercando di trovare una soluzione in cui non devo creare una nuova classe per ogni nodo anche se ho bisogno di un solo campo.
kenske

-7

Ciao, ecco il codice funzionante completo.

// JUNIT TEST CLASS

public class sof {

@Test
public void test() {

    Brand b = new Brand();
    b.id=1;
    b.name="RIZZE";

    Product p = new Product();
    p.brand=b;
    p.id=12;
    p.name="bigdata";


    //mapper
    ObjectMapper o = new ObjectMapper();
    o.registerSubtypes(Brand.class);
    o.registerSubtypes(Product.class);
    o.setVisibility(PropertyAccessor.FIELD, Visibility.ANY);

    String json=null;
    try {
        json = o.writeValueAsString(p);
        assertTrue(json!=null);
        logger.info(json);

        Product p2;
        try {
            p2 = o.readValue(json, Product.class);
            assertTrue(p2!=null);
            assertTrue(p2.id== p.id);
            assertTrue(p2.name.compareTo(p.name)==0);
            assertTrue(p2.brand.id==p.brand.id);
            logger.info("SUCCESS");
        } catch (IOException e) {

            e.printStackTrace();
            fail(e.toString());
        }




    } catch (JsonProcessingException e) {

        e.printStackTrace();
        fail(e.toString());
    }


}
}




**// Product.class**
    public class Product {
    protected int id;
    protected String name;

    @JsonProperty("brand") //not necessary ... but written
    protected Brand brand;

}

    **//Brand class**
    public class Brand {
    protected int id;
    protected String name;     
}

//Console.log di junit testcase

2016-05-03 15:21:42 396 INFO  {"id":12,"name":"bigdata","brand":{"id":1,"name":"RIZZE"}} / MReloadDB:40 
2016-05-03 15:21:42 397 INFO  SUCCESS / MReloadDB:49 

Sintesi completa: https://gist.github.com/jeorfevre/7c94d4b36a809d4acf2f188f204a8058


1
La risposta JSON effettiva che sto cercando di mappare è molto complessa. Ha molti nodi e sottonodi e creare una classe per ciascuno sarebbe molto doloroso, ancora di più quando nella maggior parte dei casi ho solo bisogno di un singolo campo da un insieme di tripli nodi annidati. Non c'è un modo per ottenere solo il singolo campo senza dover creare un sacco di nuove classi?
kenske

apri un nuovo ticket sof e condividilo con me, posso aiutarti in questo. Condividi la struttura JSON che desideri mappare, per favore. :)
jeorfevre
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.