Bagaimana cara menyertakan JSON mentah dalam objek menggunakan Jackson?

102

Saya mencoba memasukkan JSON mentah di dalam objek Java ketika objek tersebut (de) diserialisasi menggunakan Jackson. Untuk menguji fungsionalitas ini, saya menulis tes berikut:

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

Kode mengeluarkan baris berikut:

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

JSON persis seperti yang saya inginkan. Sayangnya, kode gagal dengan pengecualian saat mencoba membaca JSON kembali ke objek. Berikut pengecualiannya:

org.codehaus.jackson.map.JsonMappingException: Tidak dapat mendelialisasi instance java.lang.String dari token START_OBJECT di [Sumber: java.io.StringReader@d70d7a; baris: 1, kolom: 13] (melalui rantai referensi: com.tnal.prism.cobalt.gather.testing.Pojo ["bar"])

Mengapa Jackson berfungsi dengan baik di satu arah tetapi gagal saat pergi ke arah lain? Sepertinya itu harus dapat mengambil keluarannya sendiri sebagai masukan lagi. Saya tahu apa yang saya coba lakukan tidak ortodoks (saran umumnya adalah membuat objek dalam baryang memiliki properti bernama A), tetapi saya sama sekali tidak ingin berinteraksi dengan JSON ini. Kode saya bertindak sebagai penerusan untuk kode ini - Saya ingin mengambil JSON ini dan mengirimkannya kembali tanpa menyentuh apa pun, karena ketika JSON berubah, saya tidak ingin kode saya perlu dimodifikasi.

Terima kasih atas sarannya.

EDIT: Menjadikan Pojo kelas statis, yang menyebabkan kesalahan berbeda.

bhilstrom
sumber

Jawaban:

64

@JsonRawValue ditujukan hanya untuk sisi serialisasi, karena arah sebaliknya agak lebih sulit untuk ditangani. Akibatnya itu ditambahkan untuk memungkinkan menyuntikkan konten yang telah dikodekan sebelumnya.

Saya kira akan mungkin untuk menambahkan dukungan untuk mundur, meskipun itu akan sangat aneh: konten harus diurai, dan kemudian ditulis ulang kembali ke bentuk "mentah", yang mungkin atau mungkin tidak sama (karena kutipan karakter mungkin berbeda). Ini untuk kasus umum. Tapi mungkin masuk akal untuk beberapa subset masalah.

Tapi saya pikir solusi untuk kasus spesifik Anda akan menentukan jenis sebagai 'java.lang.Object', karena ini harus berfungsi dengan baik: untuk serialisasi, String akan menjadi keluaran apa adanya, dan untuk deserialisasi, itu akan dideserialisasi sebagai sebuah peta. Sebenarnya Anda mungkin ingin memiliki pengambil / penyetel yang terpisah jika demikian; getter akan mengembalikan String untuk serialisasi (dan membutuhkan @JsonRawValue); dan penyetel akan memilih Map atau Object. Anda dapat menyandikan ulang ke String jika itu masuk akal.

StaxMan
sumber
1
Ini bekerja seperti pesona; lihat tanggapan saya untuk kode ( format di komentar adalah awgul ).
yves amsellem
Saya memiliki kasus penggunaan yang berbeda untuk ini. Sepertinya jika kita tidak ingin menghasilkan banyak string sampah di deser / ser, kita harus bisa memasukkan string seperti itu. Saya melihat utas yang melacak ini, tetapi tampaknya tidak ada dukungan asli yang memungkinkan. Silahkan lihat pada markmail.org/message/...
Sid
@Sid tidak ada cara untuk melakukan itu DAN tokenisasi keduanya secara efisien. Untuk mendukung penerusan token yang belum diproses akan membutuhkan pemeliharaan status tambahan, yang membuat penguraian "reguler" menjadi kurang efisien. Ini seperti pengoptimalan antara kode biasa dan pengecualian melempar: untuk mendukung yang terakhir menambahkan overhead untuk yang pertama. Jackson tidak dirancang untuk mencoba menyediakan masukan yang belum diproses; alangkah baiknya untuk memilikinya (untuk pesan kesalahan, juga), tetapi akan membutuhkan pendekatan yang berbeda.
StaxMan
55

Mengikuti jawaban @StaxMan , saya telah membuat karya berikut seperti pesona:

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

Dan, untuk setia pada pertanyaan awal, inilah tes kerjanya:

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
sumber
1
Saya tidak mencoba, tetapi seharusnya berhasil: 1) buat node JsonNode daripada Object json 2) gunakan node.asText () daripada toString (). Saya tidak yakin tentang yang kedua.
Vadim Kirilchuk
Aku bertanya-tanya mengapa getJson()mengembalikan a String. Jika itu hanya mengembalikan JsonNodeyang telah ditetapkan melalui penyetel, itu akan diserialkan seperti yang diinginkan, bukan?
sorrymissjackson
@VadimKirilchuk node.asText()mengembalikan nilai kosong yang berlawanan toString().
v.ladynev
36

Saya bisa melakukan ini dengan deserializer khusus (dipotong dan ditempel dari sini )

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
sumber
6
Luar biasa betapa sederhananya. IMO ini harus menjadi jawaban resmi. Saya mencoba dengan struktur yang sangat kompleks yang berisi array, subobjects, dll. Mungkin Anda mengedit jawaban Anda dan menambahkan bahwa anggota String yang akan dideserialisasi harus dianotasi oleh @JsonDeserialize (using = KeepAsJsonDeserialzier.class). (dan perbaiki kesalahan ketik pada nama kelas Anda ;-)
Heri
ini berfungsi untuk Deserializion. Bagaimana dengan Serialisasi json mentah menjadi pojo? Bagaimana itu akan tercapai
xtrakBandit
4
@xtrakBandit untuk Serialisasi, gunakan@JsonRawValue
smartwjw
Ini bekerja seperti pesona. Terima kasih Roy dan @Heri ..com. Kombinasi postingan ini bersama dengan komentar Heri adalah jawaban terbaik.
Michal
Solusi sederhana dan rapi. Saya setuju dengan @Heri
mahesh nanayakkara
18

@Jsonter dapat membantu. Lihat contoh saya ('data' seharusnya berisi JSON yang tidak diurai):

class Purchase
{
    String data;

    @JsonProperty("signature")
    String signature;

    @JsonSetter("data")
    void setData(JsonNode data)
    {
        this.data = data.toString();
    }
}
anagaf
sumber
3
Menurut Metode dokumentasi JsonNode.toString () yang akan menghasilkan representasi node yang dapat dibaca pengembang; yang mungkin <b> atau mungkin tidak </b> JSON yang valid. Jadi implementasi ini sebenarnya sangat beresiko.
Piotr
@Piotr javadoc sekarang mengatakan "Metode yang akan menghasilkan (seperti Jackson 2.10) JSON yang valid menggunakan pengaturan default databind, sebagai String"
bernie
4

Menambah jawaban bagus Roy Truelove , berikut adalah cara menyuntikkan custom deserialiser sebagai respons terhadap tampilan :@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
sumber
ini tidak berfungsi di Jackson 2.9. Sepertinya itu rusak karena sekarang menggunakan properti lama di PropertyBasedCreator.construct alih-alih menggantinya
dant3
3

Ini adalah masalah dengan kelas batin Anda. The Pojokelas adalah kelas batin non-statis kelas pengujian Anda, dan Jackson tidak bisa instantiate kelas. Jadi itu bisa membuat serial, tapi tidak deserialize.

Definisikan ulang kelas Anda seperti ini:

public static class Pojo {
    public String foo;

    @JsonRawValue
    public String bar;
}

Perhatikan penambahan static

skaffman
sumber
Terima kasih. Itu membuat saya selangkah lebih maju, tetapi sekarang saya mendapatkan kesalahan yang berbeda. Saya akan memperbarui posting asli dengan kesalahan baru.
Bhilstrom
3

Solusi mudah ini berhasil untuk saya:

public class MyObject {
    private Object rawJsonValue;

    public Object getRawJsonValue() {
        return rawJsonValue;
    }

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

Jadi saya bisa menyimpan nilai mentah JSON dalam variabel rawJsonValue dan kemudian tidak ada masalah untuk menghilangkannya (sebagai objek) dengan bidang lain kembali ke JSON dan mengirim melalui REST saya. Menggunakan @JsonRawValue tidak membantu saya karena JSON yang disimpan dideserialisasi sebagai String, bukan sebagai objek, dan bukan itu yang saya inginkan.

Pavol Golias
sumber
3

Ini bahkan berfungsi di entitas 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());
}

Idealnya ObjectMapper dan bahkan JsonFactory berasal dari konteks dan dikonfigurasi untuk menangani JSON Anda dengan benar (standar atau dengan nilai non-standar seperti float 'Infinity' misalnya).

Georg
sumber
1
Menurut JsonNode.toString()dokumentasi Method that will produce developer-readable representation of the node; which may <b>or may not</b> be as valid JSON.Jadi ini sebenarnya implementasi yang sangat berisiko.
Piotr
Hai @Piotr, terima kasih atas petunjuknya. Anda benar tentu saja, ini menggunakan JsonNode.asText()internal dan akan mengeluarkan Infinity dan nilai JSON non-standar lainnya.
Georg
@Piotr javadoc sekarang mengatakan "Metode yang akan menghasilkan (seperti Jackson 2.10) JSON yang valid menggunakan pengaturan default databind, sebagai String"
bernie
2

Berikut adalah contoh kerja lengkap tentang cara menggunakan modul Jackson untuk membuat @JsonRawValuepekerjaan dua arah (serialisasi dan deserialisasi):

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

Kemudian Anda dapat mendaftarkan modul setelah membuat 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
sumber
Apakah ada hal lain selain di atas yang harus Anda lakukan? Saya telah menemukan bahwa metode deserialize dari JsonRawValueDeserializer tidak pernah dipanggil oleh ObjectMapper
Michael Coxon
@MichaelCoxon Apakah Anda berhasil membuatnya bekerja? Satu hal yang menyebabkan saya mengalami masalah di masa lalu adalah menggunakan anotasi dari org.codehaus.jacksonpaket tanpa menyadarinya. Pastikan semua impor Anda berasal com.fasterxml.jackson.
Helder Pereira
1

Saya memiliki masalah yang sama persis. Saya menemukan solusinya dalam posting ini: Parse JSON tree ke kelas biasa menggunakan Jackson atau alternatifnya

Lihat jawaban terakhir. Dengan menentukan penyetel khusus untuk properti yang menggunakan JsonNode sebagai parameter dan memanggil metode toString di jsonNode untuk menyetel properti String, semuanya berhasil.

Alex Kubity
sumber
1

Menggunakan objek bekerja dengan baik dua arah ... Metode ini memiliki sedikit overhead deserialisasi nilai mentah dalam dua kali.

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

Saya memiliki masalah serupa, tetapi menggunakan daftar dengan banyak itens JSON ( List<String>).

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

Saya mengelola serialisasi menggunakan @JsonRawValueanotasi. Tapi untuk deserialization saya harus membuat custom deserializer berdasarkan saran Roy.

public class Errors {

    private Integer status;

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

}

Di bawah ini Anda dapat melihat deserializer "Daftar" saya.

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
sumber