java - only - ¿Cómo puedo incluir JSON sin formato en un objeto usando Jackson?
jackson nested json (12)
Estoy tratando de incluir JSON sin procesar dentro de un objeto Java cuando el objeto se (de) 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 cuando se intenta volver a leer el JSON en el objeto. Aquí está la excepción:
org.codehaus.jackson.map.JsonMappingException: no se puede deserializar la instancia de java.lang.String del token START_OBJECT en [Origen: 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 al ir en la otra dirección? Parece que debería poder 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 interno para la bar
que tenga una propiedad llamada A
), pero no quiero interactuar con este JSON en absoluto. Mi código está actuando como un paso para este código: quiero tomar este JSON y enviarlo de nuevo sin tocar nada, porque cuando el JSON cambie no quiero que mi código necesite modificaciones.
Gracias por el consejo.
EDITAR: hizo Pojo una clase estática, que estaba causando un error diferente.
@JsonRawValue está diseñado solo para el lado de serialización, ya que la dirección inversa es un poco más difícil de manejar. En efecto, se agregó para permitir la inyección de contenido precodificado.
Supongo que sería posible agregar soporte para el reverso, aunque sería bastante incómodo: el contenido tendrá que ser analizado y luego reescrito en forma "cruda", que puede ser o no ser el mismo (ya que el carácter cita puede diferenciarse). Esto para el caso general. Pero tal vez tendría sentido para un subconjunto de problemas.
Pero creo que una solución 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á tal cual, y para la deserialización, se deserializará como un mapa. En realidad, es posible que desee tener getter / setter por separado si es así; getter devolvería String para la serialización (y necesita @JsonRawValue); y setter tomaría Map u Object. Podría volver a codificarlo en una Cadena si tiene sentido.
@JsonSetter puede ayudar. Ver mi muestra (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();
}
}
Agregando a la gran answer , 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;
}
}
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 ObjectMapper
:
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JsonRawValueDeserializerModule());
String json = "{/"foo/":/"one/",/"bar/":{/"A/":false}}";
Pojo deserialized = objectMapper.readValue(json, Pojo.class);
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 bruto de JSON en la variable rawJsonValue y luego no hubo problema para deserializarlo (como objeto) con otros campos volver a JSON y enviarlos a través de mi REST. Usar @JsonRawValue no me ayudó porque el JSON almacenado se deserializó como String, no como un objeto, y eso no era lo que yo quería.
Este es un problema con tus clases internas. La clase Pojo
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.
Redefina su clase así:
public static class Pojo {
public String foo;
@JsonRawValue
public String bar;
}
Tenga en cuenta la adición de static
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());
}
Pude hacer esto con un deserializador personalizado (cortado y pegado desde here )
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();
}
}
Siguiendo la respuesta de , he hecho 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 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}
}
}
Tuve un problema similar, pero usando una lista con muchos elementos JSON ( List<String>
).
public class Errors {
private Integer status;
private List<String> jsons;
}
@JsonRawValue
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 puede 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");
}
}
Usar un objeto funciona bien en ambos sentidos ... Este método tiene un poco de sobrecarga que deserializa 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;
}
}
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 configurador personalizado para la propiedad que toma un parámetro JsonNode como parámetro y llama al método toString en jsonNode para establecer la propiedad String, todo funciona.