Jak mogę dołączyć surowy JSON do obiektu za pomocą Jacksona?

103

Próbuję dołączyć surowy kod JSON do obiektu Java, gdy obiekt jest (de) serializowany przy użyciu Jacksona. Aby przetestować tę funkcjonalność, napisałem następujący 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);
}

Kod wyświetla następujący wiersz:

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

JSON jest dokładnie taki, jak chcę, aby wszystko wyglądało. Niestety, kod kończy się niepowodzeniem z wyjątkiem podczas próby odczytania JSON z powrotem do obiektu. Oto wyjątek:

org.codehaus.jackson.map.JsonMappingException: nie można deserializować instancji java.lang.String z START_OBJECT tokenu pod adresem [Źródło: java.io.StringReader@d70d7a; wiersz: 1, kolumna: 13] (przez łańcuch odniesienia: com.tnal.prism.cobalt.gather.testing.Pojo ["bar"])

Dlaczego Jackson działa dobrze w jednym kierunku, ale zawodzi, gdy idzie w drugim? Wygląda na to, że powinien być w stanie ponownie przyjąć własne wyjście jako dane wejściowe. Wiem, że to, co próbuję zrobić, jest nieortodoksyjne (ogólną radą jest utworzenie wewnętrznego obiektu, barktóry ma właściwość o nazwie A), ale nie chcę w ogóle wchodzić w interakcje z tym JSONem. Mój kod działa jako przejście dla tego kodu - chcę pobrać ten JSON i wysłać go z powrotem bez dotykania niczego, ponieważ kiedy JSON się zmienia, nie chcę, aby mój kod wymagał modyfikacji.

Dzięki za radę.

EDYCJA: Zmieniono Pojo jako statyczną klasę, która powodowała inny błąd.

bhilstrom
źródło

Odpowiedzi:

65

@JsonRawValue jest przeznaczony tylko dla strony serializacji, ponieważ odwrotny kierunek jest nieco trudniejszy w obsłudze. W efekcie został dodany, aby umożliwić wstrzykiwanie wstępnie zakodowanej zawartości.

Wydaje mi się, że byłoby możliwe dodanie obsługi odwrócenia, chociaż byłoby to dość niewygodne: treść będzie musiała zostać przeanalizowana, a następnie ponownie zapisana z powrotem do "surowej" formy, która może być taka sama lub nie (ponieważ cytowanie znaków mogą się różnić). Dotyczy to przypadku ogólnego. Ale może miałoby to sens w przypadku jakiejś podgrupy problemów.

Ale myślę, że rozwiązaniem dla twojego konkretnego przypadku byłoby określenie typu jako `` java.lang.Object '', ponieważ powinno to działać poprawnie: w przypadku serializacji ciąg zostanie wyprowadzony bez zmian, aw przypadku deserializacji zostanie zdeserializowany jako Mapa. Właściwie możesz chcieć mieć oddzielny pobierający / ustawiający, jeśli tak; getter zwróciłby String do serializacji (i potrzebuje @JsonRawValue); a ustawiacz przyjmie Map lub Object. Możesz ponownie zakodować go na String, jeśli ma to sens.

StaxMan
źródło
1
To działa jak urok; zobacz moją odpowiedź na kod ( formatowanie w komentarzach to awgul ).
yves amsellem
Miałem do tego inny przypadek użycia. Wygląda na to, że jeśli nie chcemy generować wielu śmieci łańcuchowych w deser / ser, powinniśmy być w stanie po prostu przekazać ciąg jako taki. Widziałem wątek, który to śledził, ale wygląda na to, że natywne wsparcie nie jest możliwe. Zajrzyj na markmail.org/message/…
Sid
@Sid nie ma sposobu, aby to zrobić ORAZ tokenizację zarówno efektywnie. Obsługa przekazywania nieprzetworzonych tokenów wymagałaby dodatkowego utrzymywania stanu, co sprawia, że ​​„regularne” analizowanie jest nieco mniej wydajne. Jest to coś w rodzaju optymalizacji między zwykłym kodem a wyrzucaniem wyjątków: obsługa tego drugiego powoduje zwiększenie kosztów pierwszego. Jackson nie został zaprojektowany, aby starać się utrzymywać dostępność nieprzetworzonych danych wejściowych; byłoby miło mieć go (również w przypadku komunikatów o błędach) w pobliżu, ale wymagałoby innego podejścia.
StaxMan
55

Podążając za odpowiedzią @StaxMan , wykonałem następujące czynności:

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

Aby być wiernym pierwszemu pytaniu, oto próba robocza:

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
źródło
1
Nie próbowałem, ale powinno działać: 1) utwórz węzeł JsonNode zamiast Object json 2) użyj node.asText () zamiast toString (). Nie jestem jednak pewien co do drugiego.
Vadim Kirilchuk
Zastanawiam się, dlaczego getJson()w Stringkońcu wraca . Jeśli po prostu zwróci to, JsonNodeco zostało ustawione przez setter, zostanie serializowany zgodnie z życzeniem, nie?
sorrymissjackson
@VadimKirilchuk node.asText()zwraca przeciwną wartość pustą toString().
przeciwko ladynev,
37

Udało mi się to zrobić za pomocą niestandardowego deserializatora (wyciąć i wkleić stąd )

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
źródło
6
Niesamowite, jakie proste. IMO to powinna być oficjalna odpowiedź. Próbowałem z bardzo złożoną strukturą zawierającą tablice, podobiekty itp. Może edytujesz swoją odpowiedź i dodajesz, że element członkowski String do deserializacji powinien być oznaczony adnotacją @JsonDeserialize (przy użyciu = KeepAsJsonDeserialzier.class). (i popraw literówkę w nazwie swojej klasy ;-)
Heri
to działa w przypadku Deserializion. A co z serializacją surowego pliku json do pojo? Jak by to osiągnąć
xtrakBandit
4
@xtrakBandit do serializacji, użyj@JsonRawValue
smartwjw
To działa jak urok. Dziękuję Roy i @Heri ..com. Połączenie tego postu wraz z komentarzem Heri to najlepsza odpowiedź.
Michał
Proste i zgrabne rozwiązanie. Zgadzam się z @Heri
mahesh nanayakkara
18

@JsonSetter może pomóc. Zobacz moją próbkę („dane” powinny zawierać nieprzetworzony kod JSON):

class Purchase
{
    String data;

    @JsonProperty("signature")
    String signature;

    @JsonSetter("data")
    void setData(JsonNode data)
    {
        this.data = data.toString();
    }
}
anagaf
źródło
3
Zgodnie z dokumentacją JsonNode.toString () Metoda, która zapewni czytelną dla programistę reprezentację węzła; który może <b> lub nie </b> być prawidłowym formatem JSON. Jest to więc właściwie bardzo ryzykowna implementacja.
Piotr
@Piotr javadoc mówi teraz „Metoda, która wyprodukuje (od wersji 2.10 Jacksona) poprawny JSON przy użyciu domyślnych ustawień databind, jako String”
bernie
4

Dodając do świetnej odpowiedzi Roya Truelove , oto jak wstrzyknąć niestandardowy deserializator w odpowiedzi na pojawienie się :@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
źródło
to nie działa w Jackson 2.9. Wygląda na to, że został uszkodzony, ponieważ używa teraz starej właściwości w PropertyBasedCreator.construct zamiast zastępować jedną
dant3
3

To jest problem z twoimi wewnętrznymi klasami. PojoKlasa jest niestatyczny klasy wewnętrzny od klasy badanej i Jackson nie może instancji tej klasy. Więc może serializować, ale nie deserializować.

Przedefiniuj swoją klasę w ten sposób:

public static class Pojo {
    public String foo;

    @JsonRawValue
    public String bar;
}

Zwróć uwagę na dodanie static

skaffman
źródło
Dzięki. To posunęło mnie o krok dalej, ale teraz pojawia się inny błąd. Zaktualizuję oryginalny post o nowy błąd.
bhilstrom
3

To proste rozwiązanie zadziałało dla mnie:

public class MyObject {
    private Object rawJsonValue;

    public Object getRawJsonValue() {
        return rawJsonValue;
    }

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

Mogłem więc przechowywać surową wartość JSON w zmiennej rawJsonValue, a następnie nie było problemu z deserializacją (jako obiektu) z innymi polami z powrotem do JSON i wysłaniem przez mój REST. Używanie @JsonRawValue nie pomogło mi, ponieważ przechowywany JSON został zdeserializowany jako String, a nie jako obiekt, a nie tego chciałem.

Pavol Golias
źródło
3

Działa to nawet w jednostce 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());
}

W idealnym przypadku ObjectMapper, a nawet JsonFactory pochodzą z kontekstu i są skonfigurowane tak, aby poprawnie obsługiwać JSON (na przykład standardowe lub niestandardowe wartości, takie jak „Infinity”).

Georg
źródło
1
Zgodnie z JsonNode.toString()dokumentacją Method that will produce developer-readable representation of the node; which may <b>or may not</b> be as valid JSON.Jest to więc właściwie bardzo ryzykowna realizacja.
Piotr
Cześć @Piotr, dzięki za podpowiedź. Masz oczywiście rację, używa to JsonNode.asText()wewnętrznie i wyprowadzi Infinity i inne niestandardowe wartości JSON.
Georg
@Piotr javadoc mówi teraz „Metoda, która wyprodukuje (od wersji 2.10 Jacksona) poprawny JSON przy użyciu domyślnych ustawień databind, jako String”
bernie
2

Oto pełny działający przykład, jak używać modułów Jacksona do @JsonRawValuepracy w obie strony (serializacja i deserializacja):

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

Następnie możesz zarejestrować moduł po utworzeniu 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
źródło
Czy jest jeszcze coś oprócz powyższego, co musisz zrobić? I okazało się, że metoda Cofnięcie z JsonRawValueDeserializer nigdy nie jest wywoływana przez ObjectMapper
Michael Coxon
@MichaelCoxon Czy udało ci się sprawić, by to zadziałało? Jedną z rzeczy, która powodowała problemy w przeszłości, było używanie adnotacji z org.codehaus.jacksonpakietu bez zdawania sobie z tego sprawy. Upewnij się, że cały import pochodzi z pliku com.fasterxml.jackson.
Helder Pereira,
1

Miałem dokładnie ten sam problem. Rozwiązanie znalazłem w tym poście: przeanalizuj drzewo JSON do zwykłej klasy przy użyciu Jacksona lub jego alternatyw

Sprawdź ostatnią odpowiedź. Po zdefiniowaniu niestandardowej metody ustawiającej dla właściwości, która przyjmuje JsonNode jako parametr i wywołuje metodę toString w jsonNode w celu ustawienia właściwości String, wszystko działa.

Alex Kubity
źródło
1

Korzystanie z obiektu działa dobrze w obie strony ... Ta metoda powoduje dwukrotną deserializację surowej wartości.

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
źródło
1

Miałem podobny problem, ale używając listy z wieloma itensami JSON ( List<String>).

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

Udało mi się serializować za pomocą @JsonRawValueadnotacji. Ale do deserializacji musiałem stworzyć niestandardowy deserializator na podstawie sugestii Roya.

public class Errors {

    private Integer status;

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

}

Poniżej możesz zobaczyć mój deserializator „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
źródło