¿Cómo asignar un valor anidado a una propiedad usando anotaciones de Jackson?

90

Digamos que estoy haciendo una llamada a una API que responde con el siguiente JSON para un producto:

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

Puedo mapear la identificación del producto y el nombre muy bien usando las anotaciones 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;
    }
}

Y luego usando el método fromJson para crear el producto:

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

Pero ahora estoy tratando de averiguar cómo tomar el nombre de la marca, que es una propiedad anidada. Esperaba que algo como esto funcionara:

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

Pero, por supuesto, no fue así. ¿Existe una manera fácil de lograr lo que quiero usando anotaciones?

La respuesta JSON real que estoy tratando de analizar es muy compleja y no quiero tener que crear una clase completamente nueva para cada subnodo, aunque solo necesito un campo.

kenske
fuente
2
Terminé usando github.com/json-path/JsonPath - Spring también lo usa debajo del capó. Por ejemplo, en su org.springframework.data.web.
deshumanizador

Respuestas:

89

Puedes lograr esto así:

String brandName;

@JsonProperty("brand")
private void unpackNameFromNestedObject(Map<String, String> brand) {
    brandName = brand.get("name");
}
Jacek Grobelny
fuente
23
¿Qué tal tres niveles de profundidad?
Robin Kanters
5
@Robin ¿No funcionaría? this.abbreviation = ((Map<String, Object>)portalCharacteristics.get("icon")).get("ticker").toString();
Marcello de Sales
El mismo método también se puede utilizar para desempaquetar varios valores ... unpackValuesFromNestedBrand - gracias
msanjay
13

Así es como manejé este problema:

Brand clase:

package org.answer.entity;

public class Brand {

    private Long id;

    private String name;

    public Brand() {

    }

    //accessors and mutators
}

Product clase:

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
}

Aquí, la brandinstancia será ignorada por Jacksondurante serializationy deserialization, ya que está anotada con @JsonIgnore.

Jacksonutilizará el método anotado con @JsonGetterfor serializationof java object en JSONformato. Entonces, el brandNameestá configurado con brand.getName().

De manera similar, Jacksonusará el método anotado con @JsonSetterfor deserializationof JSONformat en el objeto java. En este escenario, deberá crear una instancia del brandobjeto usted mismo y establecer su namepropiedad desde brandName.

Puede utilizar @Transientla anotación de persistencia con brandName, si desea que el proveedor de persistencia la ignore.

Soleado KC
fuente
1

Lo mejor es utilizar métodos de establecimiento:

JSON:

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

El método setter para lat o lng se verá así:

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

si necesita leer ambos (como lo haría normalmente), utilice un 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
fuente
-6

Para hacerlo simple ... he escrito el código ... la mayor parte se explica por sí mismo.

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
fuente
6
Esto funciona, pero estoy tratando de encontrar una solución en la que no tenga que crear una nueva clase para cada nodo, incluso si solo necesito un campo.
kenske
-7

Hola, aquí está el código de trabajo completo.

// CLASE DE PRUEBA 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 de 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 

Resumen completo: https://gist.github.com/jeorfevre/7c94d4b36a809d4acf2f188f204a8058

jeorfevre
fuente
1
La respuesta JSON real que estoy tratando de mapear es muy compleja. Tiene muchos nodos y subnodos, y crear una clase para cada uno sería muy doloroso, incluso más cuando en la mayoría de los casos solo necesito un campo de un conjunto de nodos anidados triples. ¿No hay una manera de obtener un solo campo sin tener que crear un montón de clases nuevas?
kenske
abre un nuevo ticket sof y compártelo conmigo, puedo ayudarte en esto. Comparta la estructura json que desea mapear, por favor. :)
jeorfevre