Wie kann ich mit Jackson unformatiertes JSON in ein Objekt einfügen?


102

Ich versuche, unformatiertes JSON in ein Java-Objekt aufzunehmen, wenn das Objekt mit Jackson (de) serialisiert wird. Um diese Funktionalität zu testen, habe ich folgenden Test geschrieben:

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

Der Code gibt die folgende Zeile aus:

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

Mit dem JSON möchte ich genau, dass die Dinge aussehen. Leider schlägt der Code mit einer Ausnahme fehl, wenn versucht wird, den JSON wieder in das Objekt einzulesen. Hier ist die Ausnahme:

org.codehaus.jackson.map.JsonMappingException: Instanz von java.lang.String kann nicht aus dem START_OBJECT-Token unter [Quelle: java.io.StringReader@d70d7a; Zeile: 1, Spalte: 13] (durch Referenzkette: com.tnal.prism.cobalt.gather.testing.Pojo ["bar"])

Warum funktioniert Jackson in einer Richtung gut, scheitert aber in der anderen Richtung? Es scheint, dass es in der Lage sein sollte, seine eigene Ausgabe wieder als Eingabe zu verwenden. Ich weiß, was ich versuche, ist unorthodox (der allgemeine Rat ist, ein inneres Objekt zu erstellen, für bardas eine Eigenschaft namens benannt ist A), aber ich möchte überhaupt nicht mit diesem JSON interagieren. Mein Code fungiert als Durchgang für diesen Code. Ich möchte diesen JSON aufnehmen und erneut senden, ohne etwas zu berühren, da ich bei Änderungen des JSON nicht möchte, dass mein Code geändert werden muss.

Danke für den Hinweis.

BEARBEITEN: Pojo wurde zu einer statischen Klasse gemacht, die einen anderen Fehler verursachte.

Antworten:


64

@JsonRawValue ist nur für die Serialisierungsseite vorgesehen, da die umgekehrte Richtung etwas schwieriger zu handhaben ist. Tatsächlich wurde es hinzugefügt, um das Injizieren von vorcodiertem Inhalt zu ermöglichen.

Ich denke, es wäre möglich, Unterstützung für das Umkehren hinzuzufügen, obwohl dies ziemlich umständlich wäre: Der Inhalt muss analysiert und dann in die "rohe" Form zurückgeschrieben werden, die möglicherweise dieselbe ist oder nicht (seit dem Zitieren von Zeichen) könnte abweichen). Dies für den allgemeinen Fall. Aber vielleicht wäre es für eine Teilmenge von Problemen sinnvoll.

Ich denke jedoch, dass eine Problemumgehung für Ihren speziellen Fall darin besteht, den Typ als 'java.lang.Object' anzugeben, da dies in Ordnung sein sollte: Für die Serialisierung wird String so ausgegeben, wie er ist, und für die Deserialisierung wird er als so deserialisiert eine Karte. In diesem Fall möchten Sie möglicherweise einen separaten Getter / Setter haben. getter würde String zur Serialisierung zurückgeben (und benötigt @JsonRawValue); und Setter würde entweder Karte oder Objekt nehmen. Sie können es in einen String umcodieren, wenn dies sinnvoll ist.


1
Das funktioniert wie ein Zauber; Siehe meine Antwort für Code ( Formatierung in den Kommentaren ist awgul ).
Yves Amsellem

Ich hatte dafür einen anderen Anwendungsfall. Es scheint, als ob wir, wenn wir nicht viel String-Müll im Deser / Ser generieren wollen, in der Lage sein sollten, nur einen String als solchen zu durchlaufen. Ich habe einen Thread gesehen, der dies verfolgt hat, aber es scheint, dass keine native Unterstützung möglich ist. Werfen
Sid

@Sid gibt es keine Möglichkeit, dies UND Tokenisierung effizient durchzuführen. Um das Weiterleiten von unverarbeiteten Token zu unterstützen, wäre eine zusätzliche Zustandserhaltung erforderlich, was das "regelmäßige" Parsen etwas weniger effizient macht. Es ist eine Art Optimierung zwischen regulärem Code und Auslösen von Ausnahmen: Um Letzteres zu unterstützen, wird der Aufwand für Ersteres erhöht. Jackson wurde nicht entwickelt, um zu versuchen, unverarbeitete Eingaben verfügbar zu halten. Es wäre schön, es (auch für Fehlermeldungen) zu haben, würde aber einen anderen Ansatz erfordern.
StaxMan

55

Nach der Antwort von @StaxMan habe ich Folgendes wie einen Zauber gemacht:

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

Und um der Ausgangsfrage treu zu bleiben, hier der Arbeitstest:

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
Ich habe es nicht versucht, aber es sollte funktionieren: 1) einen JsonNode-Knoten anstelle von Object json erstellen 2) node.asText () anstelle von toString () verwenden. Bei der zweiten bin ich mir allerdings nicht sicher.
Vadim Kirilchuk

Ich frage mich, warum ein a doch getJson()zurückkommt String. Wenn es nur JsonNodedas zurückgibt, was über den Setter eingestellt wurde, wird es wie gewünscht serialisiert, nein?
Sorrymissjackson

@VadimKirilchuk node.asText()gibt den leeren Wert gegenüber zurück toString().
v.ladynev

36

Ich konnte dies mit einem benutzerdefinierten Deserializer tun (von hier aus ausschneiden und einfügen )

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
Erstaunlich, wie einfach. IMO sollte dies die offizielle Antwort sein. Ich habe es mit einer sehr komplexen Struktur versucht, die Arrays, Unterobjekte usw. enthält. Vielleicht bearbeiten Sie Ihre Antwort und fügen hinzu, dass das zu deserialisierende String-Mitglied mit @JsonDeserialize (using = KeepAsJsonDeserialzier.class) kommentiert werden soll. (und korrigiere den Tippfehler in deinem Klassennamen ;-)
Heri

Dies funktioniert für Deserializion. Wie wäre es mit der Serialisierung von rohem Json in ein Pojo? Wie würde das erreicht werden
xtrakBandit

4
@xtrakBandit für die Serialisierung, verwenden Sie@JsonRawValue
smartwjw

Das funktioniert wie ein Zauber. Vielen Dank, Roy und @Heri. Die Kombination dieses Beitrags mit Heris Kommentar ist imho die beste Antwort.
Michal

Einfache und saubere Lösung. Ich bin mit @Heri
Mahesh Nanayakkara

18

@JsonSetter kann helfen. Siehe mein Beispiel ('Daten' sollen nicht analysiertes JSON enthalten):

class Purchase
{
    String data;

    @JsonProperty("signature")
    String signature;

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

3
Gemäß der Dokumentationsmethode von JsonNode.toString (), die eine vom Entwickler lesbare Darstellung des Knotens erzeugt; Dies kann <b> sein oder nicht </ b> als gültiger JSON. Das ist also eigentlich eine sehr riskante Implementierung.
Piotr

@Piotr der Javadoc sagt jetzt "Methode, die (ab Jackson 2.10) gültigen JSON unter Verwendung der Standardeinstellungen der Datenbindung als String erzeugt"
bernie

4

Zusätzlich zu Roy Trueloves großartiger Antwort können Sie den benutzerdefinierten Deserialisator wie folgt injizieren @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;
    }
}

Dies funktioniert in Jackson 2.9 nicht. Sieht so aus, als wäre es kaputt
gegangen,

3

Dies ist ein Problem mit Ihren inneren Klassen. Die PojoKlasse ist eine nicht statische innere Klasse Ihrer Testklasse, und Jackson kann diese Klasse nicht instanziieren. So kann es serialisieren, aber nicht deserialisieren.

Definieren Sie Ihre Klasse folgendermaßen neu:

public static class Pojo {
    public String foo;

    @JsonRawValue
    public String bar;
}

Beachten Sie die Hinzufügung von static


Vielen Dank. Das hat mich einen Schritt weiter gebracht, aber jetzt bekomme ich einen anderen Fehler. Ich werde den ursprünglichen Beitrag mit dem neuen Fehler aktualisieren.
Bhilstrom

3

Diese einfache Lösung hat bei mir funktioniert:

public class MyObject {
    private Object rawJsonValue;

    public Object getRawJsonValue() {
        return rawJsonValue;
    }

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

So konnte ich den Rohwert von JSON in der Variablen rawJsonValue speichern, und dann war es kein Problem, ihn (als Objekt) mit anderen Feldern zurück an JSON zu deserialisieren und über meinen REST zu senden. Die Verwendung von @JsonRawValue hat mir nicht geholfen, da gespeichertes JSON als String deserialisiert wurde, nicht als Objekt, und das war nicht das, was ich wollte.


3

Dies funktioniert sogar in einer JPA-Entität:

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

Im Idealfall stammen der ObjectMapper und sogar JsonFactory aus dem Kontext und sind so konfiguriert, dass Ihr JSON korrekt behandelt wird (Standard- oder Nicht-Standardwerte wie z. B. 'Infinity'-Floats).


1
Laut JsonNode.toString()Dokumentation ist Method that will produce developer-readable representation of the node; which may <b>or may not</b> be as valid JSON.dies also eine sehr riskante Implementierung.
Piotr

Hallo @Piotr, danke für den Hinweis. Sie haben natürlich Recht, dies verwendet JsonNode.asText()intern und gibt Infinity- und andere nicht standardmäßige JSON-Werte aus.
Georg

@Piotr der Javadoc sagt jetzt "Methode, die (ab Jackson 2.10) gültigen JSON unter Verwendung der Standardeinstellungen der Datenbindung als String erzeugt"
bernie

2

Hier ist ein voll funktionsfähiges Beispiel für die Verwendung von Jackson-Modulen, um in @JsonRawValuebeide Richtungen zu arbeiten (Serialisierung und Deserialisierung):

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

Anschließend können Sie das Modul nach dem Erstellen des registrieren ObjectMapper :

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

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

Gibt es noch etwas anderes, was Sie tun müssen? Ich habe festgestellt, dass die Deserialisierungsmethode des JsonRawValueDeserializer vom ObjectMapper nie aufgerufen wird
Michael Coxon

@MichaelCoxon Hast du es geschafft, dass es funktioniert? Eine Sache, die mir in der Vergangenheit Probleme bereitete, war die Verwendung von Anmerkungen aus dem org.codehaus.jacksonPaket, ohne es zu merken. Stellen Sie sicher, dass alle Ihre Importe von kommen com.fasterxml.jackson.
Helder Pereira


1

Die Verwendung eines Objekts funktioniert in beide Richtungen einwandfrei ... Bei dieser Methode wird der Rohwert in zwei Schritten deserialisiert.

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

Ich hatte ein ähnliches Problem, verwendete aber eine Liste mit vielen JSON-itens ( List<String>).

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

Ich habe die Serialisierung mithilfe der @JsonRawValueAnmerkung verwaltet. Für die Deserialisierung musste ich jedoch einen benutzerdefinierten Deserializer erstellen, der auf Roys Vorschlag basiert.

public class Errors {

    private Integer status;

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

}

Unten sehen Sie meinen Deserializer "List".

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");
    }
}
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.