Come posso includere JSON grezzo in un oggetto usando Jackson?


102

Sto cercando di includere JSON non elaborato all'interno di un oggetto Java quando l'oggetto è (de) serializzato utilizzando Jackson. Per testare questa funzionalità, ho scritto il seguente test:

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

Il codice restituisce la seguente riga:

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

Il JSON è esattamente come voglio che le cose appaiano. Sfortunatamente, il codice non riesce con un'eccezione quando si tenta di leggere nuovamente il JSON nell'oggetto. Ecco l'eccezione:

org.codehaus.jackson.map.JsonMappingException: impossibile deserializzare l'istanza di java.lang.String dal token START_OBJECT in [Origine: java.io.StringReader@d70d7a; riga: 1, colonna: 13] (attraverso la catena di riferimento: com.tnal.prism.cobalt.gather.testing.Pojo ["bar"])

Perché Jackson funziona bene in una direzione ma fallisce quando va nell'altra direzione? Sembra che dovrebbe essere in grado di prendere di nuovo il proprio output come input. So che quello che sto cercando di fare non è ortodosso (il consiglio generale è di creare un oggetto interno barche abbia una proprietà denominata A), ma non voglio affatto interagire con questo JSON. Il mio codice funge da pass-through per questo codice: voglio prendere questo JSON e inviarlo di nuovo senza toccare nulla, perché quando il JSON cambia non voglio che il mio codice abbia bisogno di modifiche.

Grazie per il consiglio.

EDIT: ha reso Pojo una classe statica, che causava un errore diverso.

Risposte:


64

@JsonRawValue è inteso solo per la serializzazione, poiché la direzione inversa è un po 'più complicata da gestire. In effetti è stato aggiunto per consentire l'iniezione di contenuto precodificato.

Immagino che sarebbe possibile aggiungere il supporto per il contrario, anche se sarebbe piuttosto imbarazzante: il contenuto dovrà essere analizzato e quindi riscritto in una forma "grezza", che può essere o meno la stessa (dal momento che la citazione del carattere può differire). Questo per un caso generale. Ma forse avrebbe senso per qualche sottoinsieme di problemi.

Ma penso che una soluzione per il tuo caso specifico sarebbe specificare il tipo come 'java.lang.Object', poiché dovrebbe funzionare bene: per la serializzazione, String verrà emesso così com'è e per la deserializzazione verrà deserializzato come una cartina. In realtà potresti voler avere getter / setter separati se è così; getter restituirà String per la serializzazione (e necessita di @JsonRawValue); e il setter prenderebbe Map o Object. Potresti ricodificarlo in una stringa se ha senso.


1
Funziona come un fascino; vedere la mia risposta per il codice (la formattazione nei commenti è awgul ).
yves amsellem

Ho avuto un caso d'uso diverso per questo. Sembra che se non vogliamo generare un sacco di spazzatura di stringhe nel deser / ser, dovremmo essere in grado di passare solo una stringa in quanto tale. Ho visto un thread che lo ha monitorato, ma sembra che non sia possibile alcun supporto nativo. Dai
Sid

@Sid non c'è modo di farlo E la tokenizzazione in modo efficiente. Per supportare il passaggio di token non elaborati sarebbe necessario un ulteriore mantenimento dello stato, il che rende l'analisi "regolare" in qualche modo meno efficiente. È un po 'come l'ottimizzazione tra il codice normale e il lancio di eccezioni: supportare quest'ultimo aggiunge overhead al primo. Jackson non è stato progettato per cercare di mantenere disponibile l'input non elaborato; sarebbe bello averlo (anche per i messaggi di errore) in giro, ma richiederebbe un approccio diverso.
StaxMan

55

Seguendo la risposta di @StaxMan , ho realizzato le seguenti opere come un fascino:

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, per essere fedeli alla domanda iniziale, ecco il test di lavoro:

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

1
Non ho provato, ma dovrebbe funzionare: 1) crea un nodo JsonNode invece di Object json 2) usa node.asText () invece di toString (). Tuttavia non sono sicuro del secondo.
Vadim Kirilchuk

Mi chiedo perché getJson()ritorni Stringdopo tutto. Se restituisse solo JsonNodequello impostato tramite il setter, verrebbe serializzato come desiderato, no?
sorrymissjackson

@VadimKirilchuk node.asText()restituisce il valore vuoto opposto toString().
v.ladynev

36

Sono stato in grado di farlo con un deserializzatore personalizzato (tagliato e incollato da qui )

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

6
Incredibile quanto semplice. IMO questa dovrebbe essere la risposta ufficiale. Ho provato con una struttura molto complessa contenente array, suboggetti, ecc. Forse modifichi la tua risposta e aggiungi che il membro String da deserializzare dovrebbe essere annotato da @JsonDeserialize (usando = KeepAsJsonDeserialzier.class). (e correggi l'errore di battitura nel nome della tua classe ;-)
Heri

questo funziona per Deserializion. Che ne dici della serializzazione di raw json in un pojo? Come sarebbe stato realizzato
xtrakBandit

4
@xtrakBandit per la serializzazione, usa@JsonRawValue
smartwjw

Funziona come un fascino. Grazie Roy e @Heri. La combinazione di questo post insieme al commento di Heri è secondo me la migliore risposta.
Michal

Soluzione semplice e pulita. Sono d'accordo con @Heri
mahesh nanayakkara

18

@JsonSetter può aiutare. Vedi il mio esempio (i "dati" dovrebbero contenere JSON non analizzato):

class Purchase
{
    String data;

    @JsonProperty("signature")
    String signature;

    @JsonSetter("data")
    void setData(JsonNode data)
    {
        this.data = data.toString();
    }
}

3
Secondo il metodo di documentazione JsonNode.toString () che produrrà una rappresentazione leggibile dallo sviluppatore del nodo; che può <b> o meno </b> essere un JSON valido. Quindi questa è in realtà un'implementazione molto rischiosa.
Piotr

@Piotr il javadoc ora dice "Metodo che produrrà (a partire da Jackson 2.10) JSON valido utilizzando le impostazioni predefinite di databind, come String"
bernie

4

Aggiungendo alla grande risposta di Roy Truelove , ecco come iniettare il deserializzatore personalizzato in risposta alla comparsa di :@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;
    }
}

questo non funziona in Jackson 2.9. Sembra che sia stato rotto poiché ora utilizza la vecchia proprietà in PropertyBasedCreator.construct invece di sostituirne uno
dant3

3

Questo è un problema con le tue classi interiori. La Pojoclasse è una classe interna non statica della classe di test e Jackson non può istanziare quella classe. Quindi può serializzare, ma non deserializzare.

Ridefinisci la tua classe in questo modo:

public static class Pojo {
    public String foo;

    @JsonRawValue
    public String bar;
}

Notare l'aggiunta di static


Grazie. Questo mi ha fatto fare un ulteriore passo avanti, ma ora ricevo un errore diverso. Aggiornerò il post originale con il nuovo errore.
bhilstrom

3

Questa semplice soluzione ha funzionato per me:

public class MyObject {
    private Object rawJsonValue;

    public Object getRawJsonValue() {
        return rawJsonValue;
    }

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

Quindi sono stato in grado di memorizzare il valore grezzo di JSON nella variabile rawJsonValue e quindi non è stato un problema deserializzarlo (come oggetto) con altri campi su JSON e inviarlo tramite il mio REST. L'utilizzo di @JsonRawValue non mi ha aiutato perché il JSON archiviato è stato deserializzato come String, non come oggetto, e non era quello che volevo.


3

Funziona anche in un'entità 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 persino JsonFactory provengono dal contesto e sono configurati in modo da gestire correttamente il tuo JSON (standard o con valori non standard come i float 'Infinity' per esempio).


1
Secondo la JsonNode.toString()documentazione Method that will produce developer-readable representation of the node; which may <b>or may not</b> be as valid JSON.Quindi questa è in realtà un'implementazione molto rischiosa.
Piotr

Ciao @ Piotr, grazie per il suggerimento. Hai ragione, naturalmente, questo utilizza JsonNode.asText()internamente e produrrà Infinity e altri valori JSON non standard.
Georg

@Piotr il javadoc ora dice "Metodo che produrrà (a partire da Jackson 2.10) JSON valido utilizzando le impostazioni predefinite di databind, come String"
bernie

2

Ecco un esempio funzionante completo di come utilizzare i moduli Jackson per far @JsonRawValuefunzionare in entrambi i modi (serializzazione e deserializzazione):

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

Quindi puoi registrare il modulo dopo aver creato ObjectMapper:

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

String json = "{\"foo\":\"one\",\"bar\":{\"A\":false}}";
Pojo deserialized = objectMapper.readValue(json, Pojo.class);

C'è qualcos'altro oltre a quanto sopra che devi fare? Ho scoperto che il metodo deserialize di JsonRawValueDeserializer non viene mai chiamato da ObjectMapper
Michael Coxon

@MichaelCoxon Sei riuscito a farlo funzionare? Una cosa che mi ha causato problemi in passato è stata l'utilizzo delle annotazioni dal org.codehaus.jacksonpacchetto senza rendermene conto. Assicurati che tutte le tue importazioni provengano da com.fasterxml.jackson.
Helder Pereira


1

L'uso di un oggetto funziona bene in entrambi i modi ... Questo metodo ha un po 'di overhead che deserializza il valore grezzo in due volte.

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

Ho avuto un problema simile, ma utilizzando un elenco con molti JSON itens ( List<String>).

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

Ho gestito la serializzazione utilizzando l' @JsonRawValueannotazione. Ma per la deserializzazione ho dovuto creare un deserializzatore personalizzato basato sul suggerimento di Roy.

public class Errors {

    private Integer status;

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

}

Di seguito puoi vedere il mio deserializzatore "Elenco".

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");
    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.