¿Cómo puedo incluir JSON raw en un objeto usando Jackson?


Estoy tratando de incluir JSON raw dentro de un objeto Java cuando el objeto está (des)serializado usando Jackson. Para probar esta funcionalidad, escribí la siguiente prueba:

public static class Pojo {
    public String foo;

    @JsonRawValue
    public String bar;
}

@Test
public void test() throws JsonGenerationException, JsonMappingException, IOException {

    String foo = "one";
    String bar = "{\"A\":false}";

    Pojo pojo = new Pojo();
    pojo.foo = foo;
    pojo.bar = bar;

    String json = "{\"foo\":\"" + foo + "\",\"bar\":" + bar + "}";

    ObjectMapper objectMapper = new ObjectMapper();
    String output = objectMapper.writeValueAsString(pojo);
    System.out.println(output);
    assertEquals(json, output);

    Pojo deserialized = objectMapper.readValue(output, Pojo.class);
    assertEquals(foo, deserialized.foo);
    assertEquals(bar, deserialized.bar);
}

El código genera la siguiente línea:

{"foo":"one","bar":{"A":false}}

El JSON es exactamente como quiero que se vean las cosas. Desafortunadamente, el código falla con una excepción al intentar leer el JSON de nuevo en el objeto. Aquí está la excepción:

Org.codehaus.jackson.asignar.JsonMappingException: No puede deserializar instancia de java.lang.String out of START_OBJECT token en [Fuente: java.io.StringReader@d70d7a; line: 1, column: 13] (through reference chain: com.tnal.prisma.cobalto.reunir.prueba.Pojo ["bar"])

¿Por qué Jackson funciona bien en una dirección pero falla cuando va en la otra dirección? Parece que debería ser capaz de tomar su propia salida como entrada de nuevo. Sé que lo que estoy tratando de hacer es poco ortodoxo (el consejo general es crear un objeto interior para bar que tiene una propiedad llamada A), pero no quiero interactuar con este JSON en absoluto. Mi código está actuando como un paso a través de este código want Quiero tomar este JSON y enviarlo de nuevo sin tocar nada, porque cuando el JSON cambia no quiero que mi código necesite modificaciones.

Gracias por el consejo.

EDIT: Hizo de Pojo una clase estática, lo que estaba causando un error diferente.

Author: skaffman, 2011-01-24

12 answers

@JsonRawValue está diseñado para serialización-lado solamente, ya que la dirección inversa es un poco más difícil de manejar. En efecto, se añadió para permitir la inyección de contenido precodificado.

Supongo que sería posible agregar soporte para reverse, aunque eso sería bastante incómodo: el contenido tendrá que analizarse y luego reescribirse a la forma "raw", que puede o no ser la misma (ya que las citas de caracteres pueden diferir). Esto es para el caso general. Pero tal vez tendría sentido para algunos subconjunto de problemas.

Pero creo que una solución para su caso específico sería especificar el tipo como 'java.lang.Objeto', ya que esto debería funcionar ok: para la serialización, la Cadena de salida como es, y para deserialización, será deserializar como un Mapa. En realidad es posible que desee tener getter/setter separado si es así; getter devolvería Cadena para serialización( y necesita @JsonRawValue); y setter tomaría Mapa u Objeto. Usted podría re-codificarlo a una cadena si eso hace sentido.

 46
Author: StaxMan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-01-24 19:36:10

Siguiendo @StaxMan respuesta, he hecho las siguientes obras como un encanto:

public class Pojo {
  Object json;

  @JsonRawValue
  public String getJson() {
    // default raw value: null or "[]"
    return json == null ? null : json.toString();
  }

  public void setJson(JsonNode node) {
    this.json = node;
  }
}

Y, para ser fiel a la pregunta inicial, aquí está la prueba de trabajo:

public class PojoTest {
  ObjectMapper mapper = new ObjectMapper();

  @Test
  public void test() throws IOException {
    Pojo pojo = new Pojo("{\"foo\":18}");

    String output = mapper.writeValueAsString(pojo);
    assertThat(output).isEqualTo("{\"json\":{\"foo\":18}}");

    Pojo deserialized = mapper.readValue(output, Pojo.class);
    assertThat(deserialized.json.toString()).isEqualTo("{\"foo\":18}");
    // deserialized.json == {"foo":18}
  }
}
 46
Author: yves amsellem,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:03:09

Pude hacer esto con un deserializador personalizado (cortar y pegar desde aquí)

package etc;

import java.io.IOException;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.TreeNode;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;

/**
 * Keeps json value as json, does not try to deserialize it
 * @author roytruelove
 *
 */
public class KeepAsJsonDeserialzier extends JsonDeserializer<String> {

    @Override
    public String deserialize(JsonParser jp, DeserializationContext ctxt)
            throws IOException, JsonProcessingException {

        TreeNode tree = jp.getCodec().readTree(jp);
        return tree.toString();
    }
}
 30
Author: Roy Truelove,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:34:59

@JsonSetter puede ayudar. Vea mi ejemplo (se supone que' data ' contiene JSON sin analizar):

class Purchase
{
    String data;

    @JsonProperty("signature")
    String signature;

    @JsonSetter("data")
    void setData(JsonNode data)
    {
        this.data = data.toString();
    }
}
 14
Author: anagaf,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-12-05 10:37:07

Esto incluso funciona en una entidad JPA:

private String json;

@JsonRawValue
public String getJson() {
    return json;
}

public void setJson(final String json) {
    this.json = json;
}

@JsonProperty(value = "json")
public void setJsonRaw(JsonNode jsonNode) {
    setJson(jsonNode.toString());
}
 4
Author: Georg,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-07-23 20:06:00

Este es un problema con sus clases internas. La clase Pojoes una clase interna no estática de su clase de prueba, y Jackson no puede instanciar esa clase. Así que puede serializarse, pero no deserializarse.

Redefine su clase de esta manera:

public static class Pojo {
    public String foo;

    @JsonRawValue
    public String bar;
}

Tenga en cuenta la adición de static

 3
Author: skaffman,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-01-24 15:08:48

Añadiendo a la gran respuesta de Roy Truelove , esta es la forma de inyectar el deserializador personalizado en respuesta a la aparición de @JsonRawValue:

import com.fasterxml.jackson.databind.Module;

@Component
public class ModuleImpl extends Module {

    @Override
    public void setupModule(SetupContext context) {
        context.addBeanDeserializerModifier(new BeanDeserializerModifierImpl());
    }
}

import java.util.Iterator;

import com.fasterxml.jackson.annotation.JsonRawValue;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.deser.BeanDeserializerBuilder;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.deser.SettableBeanProperty;

public class BeanDeserializerModifierImpl extends BeanDeserializerModifier {
    @Override
    public BeanDeserializerBuilder updateBuilder(DeserializationConfig config, BeanDescription beanDesc, BeanDeserializerBuilder builder) {
        Iterator<SettableBeanProperty> it = builder.getProperties();
        while (it.hasNext()) {
            SettableBeanProperty p = it.next();
            if (p.getAnnotation(JsonRawValue.class) != null) {
                builder.addOrReplaceProperty(p.withValueDeserializer(KeepAsJsonDeserialzier.INSTANCE), true);
            }
        }
        return builder;
    }
}
 2
Author: Amir Abiri,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 11:33:26

Esta solución fácil funcionó para mí:

public class MyObject {
    private Object rawJsonValue;

    public Object getRawJsonValue() {
        return rawJsonValue;
    }

    public void setRawJsonValue(Object rawJsonValue) {
        this.rawJsonValue = rawJsonValue;
    }
}

Así que pude almacenar el valor sin procesar de JSON en la variable rawJsonValue y luego no fue ningún problema deserializarlo (como objeto) con otros campos de vuelta a JSON y enviarlo a través de mi REST. Usar @JsonRawValue no me ayudó porque el JSON almacenado se deserializó como cadena, no como objeto, y eso no era lo que quería.

 2
Author: Pavol Golias,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-01-30 08:05:04

Tuve exactamente el mismo problema. Encontré la solución en este post : Analizar el árbol JSON a una clase simple usando Jackson o sus alternativas

Echa un vistazo a la última respuesta. Al definir un setter personalizado para la propiedad que toma un JsonNode como parámetro y llama al método toString en el JsonNode para establecer la propiedad String, todo funciona.

 1
Author: Alex Kubity,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:10:54

Usar un objeto funciona bien en ambos sentidos... Este método tiene un poco de deserialización general del valor bruto en dos veces.

ObjectMapper mapper = new ObjectMapper();
RawJsonValue value = new RawJsonValue();
value.setRawValue(new RawHello(){{this.data = "universe...";}});
String json = mapper.writeValueAsString(value);
System.out.println(json);
RawJsonValue result = mapper.readValue(json, RawJsonValue.class);
json = mapper.writeValueAsString(result.getRawValue());
System.out.println(json);
RawHello hello = mapper.readValue(json, RawHello.class);
System.out.println(hello.data);

RawHello.java

public class RawHello {

    public String data;
}

RawJsonValue.java

public class RawJsonValue {

    private Object rawValue;

    public Object getRawValue() {
        return rawValue;
    }

    public void setRawValue(Object value) {
        this.rawValue = value;
    }
}
 1
Author: Vozzie,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-05-23 01:39:24

Aquí hay un ejemplo completo de cómo usar los módulos de Jackson para hacer que @JsonRawValue funcione en ambos sentidos (serialización y deserialización):

public class JsonRawValueDeserializerModule extends SimpleModule {

    public JsonRawValueDeserializerModule() {
        setDeserializerModifier(new JsonRawValueDeserializerModifier());
    }

    private static class JsonRawValueDeserializerModifier extends BeanDeserializerModifier {
        @Override
        public BeanDeserializerBuilder updateBuilder(DeserializationConfig config, BeanDescription beanDesc, BeanDeserializerBuilder builder) {
            builder.getProperties().forEachRemaining(property -> {
                if (property.getAnnotation(JsonRawValue.class) != null) {
                    builder.addOrReplaceProperty(property.withValueDeserializer(JsonRawValueDeserializer.INSTANCE), true);
                }
            });
            return builder;
        }
    }

    private static class JsonRawValueDeserializer extends JsonDeserializer<String> {
        private static final JsonDeserializer<String> INSTANCE = new JsonRawValueDeserializer();

        @Override
        public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
            return p.readValueAsTree().toString();
        }
    }
}

Luego puede registrar el módulo después de crear el ObjectMapper:

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JsonRawValueDeserializerModule());

String json = "{\"foo\":\"one\",\"bar\":{\"A\":false}}";
Pojo deserialized = objectMapper.readValue(json, Pojo.class);
 1
Author: Helder Pereira,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-11-08 19:15:56

Tuve un problema similar, pero usando una lista con muchos itens JSON (List<String>).

public class Errors {
    private Integer status;
    private List<String> jsons;
}

Manejé la serialización usando la anotación @JsonRawValue. Pero para la deserialización tuve que crear un deserializador personalizado basado en la sugerencia de Roy.

public class Errors {

    private Integer status;

    @JsonRawValue
    @JsonDeserialize(using = JsonListPassThroughDeserialzier.class)
    private List<String> jsons;

}

A continuación puedes ver mi deserializador de "Lista".

public class JsonListPassThroughDeserializer extends JsonDeserializer<List<String>> {

    @Override
    public List<String> deserialize(JsonParser jp, DeserializationContext cxt) throws IOException, JsonProcessingException {
        if (jp.getCurrentToken() == JsonToken.START_ARRAY) {
            final List<String> list = new ArrayList<>();
            while (jp.nextToken() != JsonToken.END_ARRAY) {
                list.add(jp.getCodec().readTree(jp).toString());
            }
            return list;
        }
        throw cxt.instantiationException(List.class, "Expected Json list");
    }
}
 1
Author: Biga,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-06-19 20:33:45