Como mapear um valor aninhado para uma propriedade usando anotações de Jackson?

88

Digamos que estou fazendo uma chamada para uma API que responde com o seguinte JSON para um produto:

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

Consigo mapear a id e o nome do produto perfeitamente usando as anotações de 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 então usando o método fromJson para criar o produto:

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

Mas agora estou tentando descobrir como obter o nome da marca, que é uma propriedade aninhada. Eu esperava que algo assim funcionasse:

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

Mas é claro que não. Existe uma maneira fácil de realizar o que desejo usando anotações?

A resposta JSON real que estou tentando analisar é muito complexa e não quero ter que criar uma nova classe inteira para cada subnó, embora precise apenas de um único campo.

Kenske
fonte
2
Acabei usando github.com/json-path/JsonPath - Spring também usa sob o capô. Por exemplo, em seu org.springframework.data.web.
desumanizador

Respostas:

87

Você pode conseguir isso assim:

String brandName;

@JsonProperty("brand")
private void unpackNameFromNestedObject(Map<String, String> brand) {
    brandName = brand.get("name");
}
Jacek Grobelny
fonte
23
Que tal três níveis de profundidade?
Robin Kanters de
4
@Robin Não funcionaria? this.abbreviation = ((Map<String, Object>)portalCharacteristics.get("icon")).get("ticker").toString();
Marcello de Sales
o mesmo método pode ser usado para descompactar vários valores também ... unpackValuesFromNestedBrand - obrigado
msanjay
12

Foi assim que resolvi esse 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
}

Aqui, a brandinstância será ignorada por Jacksondurante serializatione deserialization, uma vez que é anotada com @JsonIgnore.

Jacksonusará o método anotado com @JsonGetterfor serializationdo objeto java no JSONformato. Então, o brandNameestá definido com brand.getName().

Da mesma forma, Jacksonusará o método anotado com @JsonSetterfor deserializationde JSONformat no objeto java. Nesse cenário, você mesmo terá que instanciar o brandobjeto e definir sua namepropriedade a partir de brandName.

Você pode usar @Transienta anotação de persistência com brandName, se quiser que ela seja ignorada pelo provedor de persistência.

Sunny KC
fonte
1

O melhor é usar métodos setter:

JSON:

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

O método setter para lat ou lng será semelhante a:

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

se você precisar ler ambos (como faria normalmente), use um método personalizado

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

Para tornar mais simples ... eu escrevi o código ... a maior parte dele é autoexplicativo.

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]]
Srikanth Lankapalli
fonte
6
Isso funciona, mas estou tentando encontrar uma solução em que não precise criar uma nova classe para cada nó, mesmo se eu só precisar de um campo.
kenske
-6

Olá, aqui está o código de trabalho completo.

// AULA DE TESTE DE JUNIT

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 do caso de teste junit

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 

Síntese completa: https://gist.github.com/jeorfevre/7c94d4b36a809d4acf2f188f204a8058

jeorfevre
fonte
1
A resposta JSON real que estou tentando mapear é muito complexa. Ele tem muitos nós e subnós, e criar uma classe para cada um seria muito difícil, ainda mais quando, na maioria dos casos, eu preciso apenas de um único campo de um conjunto de nós aninhados triplos. Não há uma maneira de obter apenas um único campo sem ter que criar uma tonelada de novas classes?
kenske
abra um novo sof ticket e compartilhe comigo, eu posso te ajudar nisso. Compartilhe a estrutura json que você deseja mapear. :)
jeorfevre