2011-01-24 6 views
70

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

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 emette la seguente riga:

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

Il JSON è esattamente come voglio cose da cercare. Sfortunatamente, il codice fallisce con un'eccezione quando si tenta di leggere di nuovo il JSON nell'oggetto. Qui è l'eccezione:

org.codehaus.jackson.map.JsonMappingException: Impossibile deserializzare istanza di java.lang.String fuori START_OBJECT gettone in [Fonte: [email protected]; line: 1, colonna: 13] (attraverso la catena di riferimento: com.tnal.prism.cobalt.gather.testing.Pojo [ "barra"])

Perché la funzione Jackson bene in una direzione ma sicuro quando andando nella direzione opposta? Sembra che dovrebbe essere in grado di prendere di nuovo il proprio output come input. So che quello che sto cercando di fare è poco ortodosso (il consiglio generale è di creare un oggetto interno per bar che 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 cambia il JSON non voglio che il mio codice abbia bisogno di modifiche.

Grazie per il consiglio.

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

risposta

39

@JsonRawValue è stata realizzata solo la serializzazione lato, poiché la direzione inversa è un po 'più complicato da gestire. In effetti è stato aggiunto per consentire l'iniezione di contenuti pre-codificati.

Suppongo che sarebbe possibile aggiungere il supporto per la retromarcia, anche se ciò sarebbe abbastanza imbarazzante: il contenuto dovrà essere analizzato e quindi riscritto in forma "raw", che può o non può essere lo stesso (poiché la citazione dei caratteri può essere diversa). Questo per casi generali. Ma forse avrebbe senso per alcuni sottogruppi di problemi.

Ma penso che una soluzione per il caso specifico sarebbe quella di specificare il tipo 'java.lang.Object', poiché questo dovrebbe funzionare ok: per la serializzazione, String verrà emesso così com'è, e per la deserializzazione, sarà deserializzato come una mappa. In realtà si potrebbe desiderare di avere getter/setter separati in caso affermativo; getter restituirebbe String per la serializzazione (e ha bisogno di @JsonRawValue); e setter prenderebbe sia una mappa che un oggetto. Potresti ricodificarlo in una stringa se questo ha senso.

+0

Funziona come un fascino; vedi la mia risposta per il codice (_formatting nei commenti è awgul_). –

+0

Ho avuto un caso d'uso diverso per questo. Sembra che se non vogliamo generare molta spazzatura di stringa nel deser/ser, dovremmo essere in grado di passare semplicemente una stringa in quanto tale. Ho visto un thread che ha tracciato questo, ma sembra che non ci sia supporto nativo possibile. Dai un'occhiata a http://markmail.org/message/qjncoohoalre4vd2#query:+page:1+mid:6ol54pw3zsr4jbhr+state:results – Sid

+0

@Sid non c'è modo di farlo e la tokenizzazione sia in modo efficiente. Supportare il pass-through di token non processati richiederebbe un ulteriore mantenimento dello stato, il che rende l'analisi "regolare" un po 'meno efficiente. È un po 'come l'ottimizzazione tra il codice normale e il lancio di eccezioni: per supportare quest'ultimo aggiunge un sovraccarico per il precedente. Jackson non è stato progettato per cercare di mantenere disponibili gli input non elaborati; sarebbe bello averlo (anche per i messaggi di errore) in giro, ma richiederebbe un approccio diverso. – StaxMan

3

Questo è un problema con le vostre classi interne. La classe Pojo è una non-static inner class della classe di test e Jackson non può creare un'istanza di quella classe. Quindi può serializzare, ma non deserializzare.

ridefinire la classe in questo modo:

public static class Pojo { 
    public String foo; 

    @JsonRawValue 
    public String bar; 
} 

nota l'aggiunta di static

+0

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

41

seguito @StaxMan risposta, ho fatto le seguenti funziona 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, ad essere fedeli alla domanda iniziale, qui è la prova 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) creare un nodo JsonNode invece di Object json 2) utilizzare node.asText() invece di toString(). Però non sono sicuro del 2 °. –

+0

Mi chiedo perché 'getJson()' restituisca un 'String' dopo tutto. Se ha appena restituito il 'JsonNode' impostato tramite il setter, sarebbe serializzato come desiderato, no? – sorrymissjackson

+0

@VadimKirilchuk 'node.asText()' restituisce il valore vuoto opposto a 'toString()'. –

1

I aveva lo stesso identico problema Ho trovato la soluzione in questo post: Parse JSON tree to plain class using Jackson or its alternatives

Partenza l'ultima risposta. Definendo un setter personalizzato per la proprietà che accetta un parametro JsonNode come parametro e chiama il metodo toString su jsonNode per impostare la proprietà String, tutto funziona.

24

sono stato in grado di fare questo con un deserializzatore personalizzato (tagliato e incollato da 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(); 
    } 
} 
+3

Incredibile quanto semplice. IMO questa dovrebbe essere la risposta ufficiale. Ho provato con una struttura molto complessa contenente array, sottooggetti, ecc. Potresti modificare la tua risposta e aggiungere che il membro String da deserializzare dovrebbe essere annotato da @JsonDeserialize (using = KeepAsJsonDeserialzier.class). (e correggere l'errore di battitura nel nome della classe ;-) – Heri

+0

funziona per Deserializion. Che ne dici di serializzare il json crudo in un pojo? Come sarebbe stato realizzato – xtrakBandit

+3

@xtrakBandit per la serializzazione, usare '@ JsonRawValue' – smartwjw

0

Utilizzando un oggetto funziona bene in entrambi i sensi ... Questo metodo ha un po 'di overhead deserializzazione del 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; 
    } 
} 
10

@JsonSetter può aiutare. Vedere il mio campione (si suppone 'dati' per contenere unparsed JSON):

class Purchase 
{ 
    String data; 

    @JsonProperty("signature") 
    String signature; 

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

Aggiungendo a Roy Truelove s' grande answer, questo è il modo per iniettare il deserialiser 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; 
    } 
} 
1

Questa soluzione facile funzionato per me:

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

1

Questo 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) { 
    setJson(jsonNode.toString()); 
} 
0

Ecco un esempio di lavoro completa di come utilizzare i moduli Jackson per fare @JsonRawValue di lavoro in entrambe le direzioni (di 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 è possibile registrare il modulo dopo aver creato il ObjectMapper:

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

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