Como posso incluir JSON bruto em um objeto usando Jackson?

102

Estou tentando incluir JSON bruto dentro de um objeto Java quando o objeto é (des) serializado usando Jackson. Para testar essa funcionalidade, escrevi o seguinte teste:

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);
}

O código produz a seguinte linha:

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

O JSON é exatamente como eu quero que as coisas pareçam. Infelizmente, o código falha com uma exceção ao tentar ler o JSON de volta para o objeto. Aqui está a exceção:

org.codehaus.jackson.map.JsonMappingException: Não é possível desserializar a instância de java.lang.String do token START_OBJECT em [Fonte: java.io.StringReader@d70d7a; linha: 1, coluna: 13] (por meio da cadeia de referência: com.tnal.prism.cobalt.gather.testing.Pojo ["bar"])

Por que Jackson funciona bem em uma direção, mas falha na direção oposta? Parece que ele deve ser capaz de obter sua própria saída como entrada novamente. Eu sei que o que estou tentando fazer é pouco ortodoxo (o conselho geral é criar um objeto interno para barque tenha uma propriedade chamada A), mas não quero interagir com esse JSON de forma alguma. Meu código está atuando como uma passagem para este código - quero pegar este JSON e enviá-lo de volta sem tocar em nada, porque quando o JSON muda, não quero que meu código precise de modificações.

Obrigado pelo conselho.

EDIT: Tornou Pojo uma classe estática, o que estava causando um erro diferente.

Bhilstrom
fonte

Respostas:

64

@JsonRawValue destina-se apenas ao lado da serialização, já que a direção reversa é um pouco mais complicada de manipular. Com efeito, foi adicionado para permitir a injeção de conteúdo pré-codificado.

Eu acho que seria possível adicionar suporte para reverso, embora isso seja bastante estranho: o conteúdo terá que ser analisado e, em seguida, reescrito de volta para a forma "bruta", que pode ou não ser a mesma (desde a citação de caracteres podem ser diferentes). Isso para o caso geral. Mas talvez faça sentido para algum subconjunto de problemas.

Mas acho que uma solução alternativa para o seu caso específico seria especificar o tipo como 'java.lang.Object', pois isso deve funcionar bem: para serialização, String será produzida como está, e para desserialização, será desserializada como um mapa. Na verdade, você pode querer ter um getter / setter separado; getter retornaria String para serialização (e precisa de @JsonRawValue); e setter pegaria Mapa ou Objeto. Você poderia recodificá-lo para uma String se isso fizer sentido.

StaxMan
fonte
1
Isso funciona como um encanto; veja minha resposta para o código (a formatação nos comentários é inadequada ).
yves amsellem
Eu tive um caso de uso diferente para isso. Parece que se não quisermos gerar muito lixo de string no deser / ser, devemos ser capazes de apenas passar uma string como tal. Eu vi um tópico que rastreou isso, mas parece que não há suporte nativo possível. Dê uma olhada em markmail.org/message/…
Sid
@Sid, não há como fazer essa tokenização AND com eficiência. Para suportar a passagem de tokens não processados, seria necessária uma manutenção de estado adicional, o que torna a análise "regular" um pouco menos eficiente. É uma espécie de otimização entre o código regular e o lançamento de exceções: dar suporte ao último adiciona sobrecarga ao primeiro. Jackson não foi projetado para tentar manter disponíveis entradas não processadas; seria bom tê-lo (para mensagens de erro também) por perto, mas exigiria uma abordagem diferente.
StaxMan
55

Seguindo a resposta de @StaxMan , fiz os seguintes trabalhos como um 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;
  }
}

E, para ser fiel à pergunta inicial, fica aqui o teste de trabalho:

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
fonte
1
Eu não tentei, mas deve funcionar: 1) fazer um nó JsonNode em vez de Object json 2) usar node.asText () em vez de toString (). Não tenho certeza sobre o segundo, porém.
Vadim Kirilchuk
Eu me pergunto por getJson()que retorna um Stringafinal. Se ele apenas retornasse o JsonNodeque foi configurado através do setter, seria serializado conforme desejado, não?
desculpemissjackson
@VadimKirilchuk node.asText()retorna um valor vazio ao lado toString().
v.ladynev
36

Consegui fazer isso com um desserializador personalizado (cortar e colar a partir daqui )

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
fonte
6
Incrível como é simples. IMO esta deve ser a resposta oficial. Tentei com uma estrutura muito complexa contendo matrizes, subobjetos, etc. Talvez você edite sua resposta e adicione que o membro String a ser desserializado deve ser anotado por @JsonDeserialize (usando = KeepAsJsonDeserialzier.class). (e corrija o erro de digitação no nome da classe ;-)
Heri
isso funciona para desserialização. Que tal para serialização de json bruto em um pojo? Como isso seria realizado
xtrakBandit
4
@xtrakBandit para serialização, use@JsonRawValue
smartwjw
Isso funciona como um encanto. Obrigado Roy e @Heri ..combination deste post junto com o comentário de Heri é imho a melhor resposta.
Michal
Solução simples e limpa. Eu concordo com @Heri
mahesh nanayakkara
18

@JsonSetter pode ajudar. Veja meu exemplo ('dados' devem conter JSON não analisado):

class Purchase
{
    String data;

    @JsonProperty("signature")
    String signature;

    @JsonSetter("data")
    void setData(JsonNode data)
    {
        this.data = data.toString();
    }
}
anagaf
fonte
3
De acordo com o método de documentação JsonNode.toString (), que produzirá uma representação legível pelo desenvolvedor do nó; que pode <b> ou não </b> ser um JSON válido. Portanto, esta implementação é realmente muito arriscada.
Piotr de
@Piotr o javadoc agora diz "Método que irá produzir (a partir de Jackson 2.10) JSON válido usando configurações padrão de databind, como String"
bernie
4

Além da ótima resposta de Roy Truelove , esta é como injetar o desserializador personalizado em resposta à aparência 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
fonte
isso não funciona no Jackson 2.9. Parece que foi quebrado, pois agora usa uma propriedade antiga em PropertyBasedCreator.construct em vez de substituir uma
dant3
3

Este é um problema com suas classes internas. A Pojoclasse é uma classe interna não estática de sua classe de teste e Jackson não pode instanciar essa classe. Portanto, ele pode serializar, mas não desserializar.

Redefina sua classe assim:

public static class Pojo {
    public String foo;

    @JsonRawValue
    public String bar;
}

Observe a adição de static

skaffman
fonte
Obrigado. Isso me deu um passo adiante, mas agora estou recebendo um erro diferente. Vou atualizar a postagem original com o novo erro.
bhilstrom
3

Esta solução fácil funcionou para mim:

public class MyObject {
    private Object rawJsonValue;

    public Object getRawJsonValue() {
        return rawJsonValue;
    }

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

Então, consegui armazenar o valor bruto do JSON na variável rawJsonValue e não foi problema desserializá-lo (como objeto) com outros campos de volta para JSON e enviar por meio do meu REST. Usar @JsonRawValue não me ajudou porque o JSON armazenado foi desserializado como String, não como objeto, e não era isso que eu queria.

Pavol Golias
fonte
3

Isso funciona até mesmo em uma entidade 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, o ObjectMapper e mesmo JsonFactory são do contexto e são configurados de forma a manipular seu JSON corretamente (padrão ou com valores não padrão como floats 'Infinity' por exemplo).

Georg
fonte
1
De acordo com a JsonNode.toString()documentação Method that will produce developer-readable representation of the node; which may <b>or may not</b> be as valid JSON.Portanto, esta implementação é realmente muito arriscada.
Piotr de
Olá @Piotr, obrigado pela dica. Você está certo, é claro, isso usa o JsonNode.asText()interno e produzirá o Infinity e outros valores JSON não padrão.
Georg
@Piotr o javadoc agora diz "Método que irá produzir (a partir de Jackson 2.10) JSON válido usando configurações padrão de databind, como String"
bernie
2

Aqui está um exemplo completo de como usar os módulos Jackson para fazer o @JsonRawValuetrabalho de ambas as maneiras (serialização e desserialização):

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();
        }
    }
}

Então você pode registrar o módulo depois de criar 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
fonte
Há algo mais além do acima que você precisa fazer? Descobri que o método de desserialização do JsonRawValueDeserializer nunca é chamado pelo ObjectMapper
Michael Coxon
@MichaelCoxon Você conseguiu fazer funcionar? Uma coisa que me causou problemas no passado foi usar anotações do org.codehaus.jacksonpacote sem perceber. Certifique-se de que todas as suas importações vêm com.fasterxml.jackson.
Helder Pereira
1

Eu tinha exatamente o mesmo problema. Encontrei a solução neste post: Analisar árvore JSON para classe simples usando Jackson ou suas alternativas

Confira a última resposta. Ao definir um configurador personalizado para a propriedade que recebe um JsonNode como parâmetro e chama o método toString no jsonNode para definir a propriedade String, tudo funciona.

Alex Kubity
fonte
1

Usar um objeto funciona bem em ambos os sentidos ... Este método tem um pouco de sobrecarga desserializando o valor bruto em duas vezes.

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
fonte
1

Tive um problema parecido, mas usando uma lista com muitos itens JSON ( List<String>).

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

Eu gerenciei a serialização usando a @JsonRawValueanotação. Mas, para a desserialização, tive que criar um desserializador personalizado com base na sugestão de Roy.

public class Errors {

    private Integer status;

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

}

Abaixo você pode ver o meu desserializador "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
fonte