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

102

Estoy tratando de incluir JSON sin procesar dentro de un objeto Java cuando el objeto se (des) serializa 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 volver a leer el JSON en el objeto. Esta es la excepción:

org.codehaus.jackson.map.JsonMappingException: no se puede deserializar la instancia de java.lang.String del token START_OBJECT en [Fuente: java.io.StringReader@d70d7a; línea: 1, columna: 13] (a través de la cadena de referencia: com.tnal.prism.cobalt.gather.testing.Pojo ["bar"])

¿Por qué Jackson funciona bien en una dirección pero falla cuando va en la otra dirección? Parece que debería poder tomar su propia salida como entrada nuevamente. Sé que lo que estoy tratando de hacer es poco ortodoxo (el consejo general es crear un objeto interno barque tenga una propiedad nombrada A), pero no quiero interactuar con este JSON en absoluto. Mi código está actuando como un paso a través para este código: 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.

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

bhilstrom
fuente

Respuestas:

64

@JsonRawValue está diseñado solo para el lado de la serialización, ya que la dirección inversa es un poco más complicada de manejar. De hecho, se agregó para permitir la inyección de contenido precodificado.

Supongo que sería posible agregar soporte para reversa, aunque eso sería bastante incómodo: el contenido tendrá que ser analizado y luego reescrito a la forma "sin procesar", que puede o no ser lo mismo (ya que las citas de caracteres puede diferenciarse). Esto para el caso general. Pero quizás tenga sentido para algún subconjunto de problemas.

Pero creo que una solución alternativa para su caso específico sería especificar el tipo como 'java.lang.Object', ya que esto debería funcionar bien: para la serialización, String se generará como está, y para la deserialización, se deserializará como un mapa. En realidad, es posible que desee tener un getter / setter separado si es así; getter devolvería String para serialización (y necesita @JsonRawValue); y el setter tomaría Mapa u Objeto. Puede volver a codificarlo en una cadena si tiene sentido.

StaxMan
fuente
1
Esto funciona a las mil maravillas; vea mi respuesta para el código (el formato en los comentarios es awgul ).
yves amsellem
Tenía un caso de uso diferente para esto. Parece que si no queremos generar mucha basura de cadena en el deser / ser, deberíamos poder simplemente pasar a través de una cadena como tal. Vi un hilo que rastreaba esto, pero parece que no hay soporte nativo posible. Eche un vistazo a markmail.org/message/…
Sid
@ Sid, no hay forma de hacer eso Y la tokenización de manera eficiente. Para admitir la transferencia de tokens sin procesar, se requeriría un mantenimiento de estado adicional, lo que hace que el análisis "regular" sea algo menos eficiente. Es una especie de optimización entre el código regular y el lanzamiento de excepciones: admitir lo último agrega una sobrecarga para el primero. Jackson no ha sido diseñado para tratar de mantener disponible la entrada sin procesar; Sería bueno tenerlo (también para mensajes de error), pero requeriría un enfoque diferente.
StaxMan
55

Siguiendo la respuesta de @StaxMan , hice los siguientes trabajos 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 fieles 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}
  }
}
yves amsellem
fuente
1
No lo intenté, pero debería funcionar: 1) haga un nodo JsonNode en lugar de Object json 2) use node.asText () en lugar de toString (). Aunque no estoy seguro sobre el segundo.
Vadim Kirilchuk
Me pregunto por qué getJson()vuelve Stringdespués de todo. Si solo devuelve el JsonNodeque se estableció a través del configurador, se serializaría como se desea, ¿no?
sorrymissjackson
@VadimKirilchuk node.asText()devuelve un valor vacío al lado toString().
v.ladynev
36

Pude hacer esto con un deserializador personalizado (cortado y pegado 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();
    }
}
Roy Truelove
fuente
6
Increíble lo simple. En mi opinión, esta debería ser la respuesta oficial. Intenté con una estructura muy compleja que contiene matrices, subobjetos, etc. Tal vez edite su respuesta y agregue que el miembro String que se deserializará debe estar anotado por @JsonDeserialize (usando = KeepAsJsonDeserialzier.class). (y corrija el error tipográfico en el nombre de su clase ;-)
Heri
esto funciona para Deserializion. ¿Qué tal para la serialización de json sin procesar en un pojo? ¿Cómo se lograría eso
XtrakBandit
4
@xtrakBandit para serialización, uso@JsonRawValue
smartwjw
Esto funciona a las mil maravillas. Gracias Roy y @Heri... La combinación de esta publicación junto con el comentario de Heri es en mi humilde opinión la mejor respuesta.
Michal
Solución simple y ordenada. Estoy de acuerdo con @Heri
mahesh nanayakkara
18

@JsonSetter puede ayudar. Vea mi muestra (se supone que los 'datos' contienen JSON sin analizar):

class Purchase
{
    String data;

    @JsonProperty("signature")
    String signature;

    @JsonSetter("data")
    void setData(JsonNode data)
    {
        this.data = data.toString();
    }
}
anagaf
fuente
3
De acuerdo con el método de documentación JsonNode.toString () que producirá una representación del nodo legible por el desarrollador; que puede <b> o no </b> ser JSON válido. Así que esta es una implementación muy arriesgada.
Piotr
@Piotr el javadoc ahora dice "Método que producirá (a partir de Jackson 2.10) JSON válido usando la configuración predeterminada de databind, como String"
bernie
4

Además de la gran respuesta de Roy Truelove , así es como 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;
    }
}
Amir Abiri
fuente
esto no funciona en Jackson 2.9. Parece que se rompió, ya que ahora usa una propiedad antigua en PropertyBasedCreator.construct en lugar de reemplazar una
dant3
3

Este es un problema con tus clases internas. La Pojoclase es una clase interna no estática de su clase de prueba y Jackson no puede crear una instancia de esa clase. Entonces puede serializar, pero no deserializar.

Redefine tu clase así:

public static class Pojo {
    public String foo;

    @JsonRawValue
    public String bar;
}

Tenga en cuenta la adición de static

skaffman
fuente
Gracias. Eso me llevó un paso más allá, pero ahora recibo un error diferente. Actualizaré la publicación original con el nuevo error.
bhilstrom
3

Esta sencilla solución 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 un 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 String, no como objeto, y eso no era lo que quería.

Pavol Golias
fuente
3

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) {
    // this leads to non-standard json, see discussion: 
    // setJson(jsonNode.toString());

    StringWriter stringWriter = new StringWriter();
    ObjectMapper objectMapper = new ObjectMapper();
    JsonGenerator generator = 
      new JsonFactory(objectMapper).createGenerator(stringWriter);
    generator.writeTree(n);
    setJson(stringWriter.toString());
}

Idealmente, ObjectMapper e incluso JsonFactory son del contexto y están configurados para manejar su JSON correctamente (estándar o con valores no estándar como flotadores 'Infinity', por ejemplo).

Georg
fuente
1
Según la JsonNode.toString()documentación Method that will produce developer-readable representation of the node; which may <b>or may not</b> be as valid JSON.Así que esta es una implementación muy arriesgada.
Piotr
Hola @Piotr, gracias por la pista. Tiene razón, por supuesto, esto usa JsonNode.asText()internamente y generará Infinity y otros valores JSON no estándar.
Georg
@Piotr el javadoc ahora dice "Método que producirá (a partir de Jackson 2.10) JSON válido usando la configuración predeterminada de databind, como String"
bernie
2

Aquí hay un ejemplo completo de cómo utilizar los módulos de Jackson para que @JsonRawValuefuncionen 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 ObjectMapper:

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

String json = "{\"foo\":\"one\",\"bar\":{\"A\":false}}";
Pojo deserialized = objectMapper.readValue(json, Pojo.class);
Helder Pereira
fuente
¿Hay algo más además de lo anterior que tenga que hacer? Descubrí que el método de deserialización de JsonRawValueDeserializer nunca es llamado por ObjectMapper
Michael Coxon
@MichaelCoxon ¿Lograste que funcionara? Una cosa que me causó problemas en el pasado fue usar anotaciones del org.codehaus.jacksonpaquete sin darme cuenta. Asegúrese de que todas sus importaciones provengan de com.fasterxml.jackson.
Helder Pereira
1

Yo tuve exactamente el mismo problema. Encontré la solución en esta publicación: Parse JSON tree to plain class usando Jackson o sus alternativas

Mira la última respuesta. Al definir un definidor personalizado para la propiedad que toma un JsonNode como parámetro y llama al método toString en jsonNode para establecer la propiedad String, todo funciona.

Alex Kubity
fuente
1

El uso de un objeto funciona bien en ambos sentidos ... Este método tiene un poco de sobrecarga deserializando el 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;
    }
}
Vozzie
fuente
1

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 @JsonRawValueanotación. 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 puede ver mi deserializador "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");
    }
}
Biga
fuente