Bagaimana cara menggunakan Serializer khusus dengan Jackson?

111

Saya memiliki dua kelas Java yang ingin saya serialkan ke JSON menggunakan Jackson:

public class User {
    public final int id;
    public final String name;

    public User(int id, String name) {
        this.id = id;
        this.name = name;
    }
}

public class Item {
    public final int id;
    public final String itemNr;
    public final User createdBy;

    public Item(int id, String itemNr, User createdBy) {
        this.id = id;
        this.itemNr = itemNr;
        this.createdBy = createdBy;
    }
}

Saya ingin membuat serial Item ke JSON ini:

{"id":7, "itemNr":"TEST", "createdBy":3}

dengan Pengguna berseri untuk hanya menyertakan id. Saya juga akan dapat mengubah semua objek pengguna ke JSON seperti:

{"id":3, "name": "Jonas", "email": "[email protected]"}

Jadi saya rasa saya perlu menulis serializer khusus Itemdan mencoba dengan ini:

public class ItemSerializer extends JsonSerializer<Item> {

@Override
public void serialize(Item value, JsonGenerator jgen,
        SerializerProvider provider) throws IOException,
        JsonProcessingException {
    jgen.writeStartObject();
    jgen.writeNumberField("id", value.id);
    jgen.writeNumberField("itemNr", value.itemNr);
    jgen.writeNumberField("createdBy", value.user.id);
    jgen.writeEndObject();
}

}

Saya membuat serial JSON dengan kode ini dari Jackson How-to: Custom Serializers :

ObjectMapper mapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule("SimpleModule", 
                                              new Version(1,0,0,null));
simpleModule.addSerializer(new ItemSerializer());
mapper.registerModule(simpleModule);
StringWriter writer = new StringWriter();
try {
    mapper.writeValue(writer, myItem);
} catch (JsonGenerationException e) {
    e.printStackTrace();
} catch (JsonMappingException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}

Tapi saya mendapatkan kesalahan ini:

Exception in thread "main" java.lang.IllegalArgumentException: JsonSerializer of type com.example.ItemSerializer does not define valid handledType() (use alternative registration method?)
    at org.codehaus.jackson.map.module.SimpleSerializers.addSerializer(SimpleSerializers.java:62)
    at org.codehaus.jackson.map.module.SimpleModule.addSerializer(SimpleModule.java:54)
    at com.example.JsonTest.main(JsonTest.java:54)

Bagaimana saya bisa menggunakan Serializer khusus dengan Jackson?


Beginilah cara saya melakukannya dengan Gson:

public class UserAdapter implements JsonSerializer<User> {

    @Override 
    public JsonElement serialize(User src, java.lang.reflect.Type typeOfSrc,
            JsonSerializationContext context) {
        return new JsonPrimitive(src.id);
    }
}

    GsonBuilder builder = new GsonBuilder();
    builder.registerTypeAdapter(User.class, new UserAdapter());
    Gson gson = builder.create();
    String json = gson.toJson(myItem);
    System.out.println("JSON: "+json);

Tapi saya perlu melakukannya dengan Jackson sekarang, karena Gson tidak memiliki dukungan untuk antarmuka.

Jonas
sumber
bagaimana / di mana Anda membuat Jackson menggunakan Serializer kustom Anda Item? Saya mengalami masalah di mana metode pengontrol saya mengembalikan objek serial standar TypeA, tetapi untuk metode pengontrol khusus lainnya, saya ingin membuat serial secara berbeda. Seperti apa kelihatannya?
Don Cheadle
Saya menulis posting tentang Cara Menulis Serializer Kustom dengan Jackson yang mungkin bisa membantu beberapa.
Sam Berry

Jawaban:

51

Seperti yang disebutkan, @JsonValue adalah cara yang bagus. Tetapi jika Anda tidak keberatan dengan serializer khusus, tidak perlu menulis satu untuk Item melainkan satu untuk Pengguna - jika demikian, itu akan sesederhana:

public void serialize(Item value, JsonGenerator jgen,
    SerializerProvider provider) throws IOException,
    JsonProcessingException {
  jgen.writeNumber(id);
}

Namun kemungkinan lain adalah untuk menerapkan JsonSerializable, dalam hal ini tidak diperlukan pendaftaran.

Untuk kesalahan; itu aneh - Anda mungkin ingin meningkatkan ke versi yang lebih baru. Tetapi juga lebih aman untuk diperpanjang org.codehaus.jackson.map.ser.SerializerBasekarena akan memiliki implementasi standar dari metode non-esensial (yaitu semuanya kecuali panggilan serialisasi aktual).

StaxMan
sumber
Dengan ini saya mendapatkan kesalahan yang sama:Exception in thread "main" java.lang.IllegalArgumentException: JsonSerializer of type com.example.JsonTest$UserSerilizer does not define valid handledType() (use alternative registration method?) at org.codehaus.jackson.map.module.SimpleSerializers.addSerializer(SimpleSerializers.java:62) at org.codehaus.jackson.map.module.SimpleModule.addSerializer(SimpleModule.java:54) at com.example.JsonTest.<init>(JsonTest.java:27) at com.exampple.JsonTest.main(JsonTest.java:102)
Jonas
Saya menggunakan versi stabil terbaru dari Jacskson, 1.8.5.
Jonas
4
Terima kasih. Aku akan melihatnya ... Ah! Ini sebenarnya sederhana (meskipun pesan kesalahan tidak baik) - Anda hanya perlu mendaftarkan serializer dengan metode yang berbeda, untuk menentukan kelas serializer itu untuk: jika tidak, itu harus mengembalikan kelas dari handleType (). Jadi gunakan 'addSerializer' yang menggunakan JavaType atau Class sebagai argumen dan seharusnya berfungsi.
StaxMan
Bagaimana jika ini tidak dijalankan?
Matej J
62

Anda dapat meletakkan @JsonSerialize(using = CustomDateSerializer.class)setiap bidang tanggal objek yang akan dibuat serial.

public class CustomDateSerializer extends SerializerBase<Date> {

    public CustomDateSerializer() {
        super(Date.class, true);
    }

    @Override
    public void serialize(Date value, JsonGenerator jgen, SerializerProvider provider)
        throws IOException, JsonProcessingException {
        SimpleDateFormat formatter = new SimpleDateFormat("EEE MMM dd yyyy HH:mm:ss 'GMT'ZZZ (z)");
        String format = formatter.format(value);
        jgen.writeString(format);
    }

}
Moesio
sumber
1
perlu diperhatikan: gunakan @JsonSerialize(contentUsing= ...)saat membuat anotasi Koleksi (misalnya @JsonSerialize(contentUsing= CustomDateSerializer.class) List<Date> dates)
coderatchet
33

Saya mencoba melakukan ini juga, dan ada kesalahan dalam kode contoh di halaman web Jackson yang gagal menyertakan type ( .class) dalam metode panggilan ke addSerializer(), yang seharusnya terbaca seperti ini:

simpleModule.addSerializer(Item.class, new ItemSerializer());

Dengan kata lain, ini adalah baris yang memberi contoh simpleModuledan menambahkan serializer (dengan baris yang salah sebelumnya dikomentari):

ObjectMapper mapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule("SimpleModule", 
                                          new Version(1,0,0,null));
// simpleModule.addSerializer(new ItemSerializer());
simpleModule.addSerializer(Item.class, new ItemSerializer());
mapper.registerModule(simpleModule);

FYI: Berikut adalah referensi untuk kode contoh yang benar: http://wiki.fasterxml.com/JacksonFeatureModules

pmhargis
sumber
9

Gunakan @JsonValue:

public class User {
    int id;
    String name;

    @JsonValue
    public int getId() {
        return id;
    }
}

@JsonValue hanya berfungsi pada metode jadi Anda harus menambahkan metode getId. Anda harus dapat melewati serializer kustom Anda sama sekali.

henrik_lundgren
sumber
2
Saya pikir ini akan memengaruhi semua upaya untuk membuat serial Pengguna, membuatnya sulit untuk mengekspos nama Pengguna melalui JSON.
Paul M
Saya tidak dapat menggunakan solusi ini, karena saya juga harus dapat membuat serialisasi semua objek pengguna dengan semua bidang. Dan solusi ini akan menghentikan serialisasi itu karena hanya id-field yang akan disertakan. Apakah tidak ada cara untuk membuat serilizer khusus untuk Jackson seperti untuk Gson?
Jonas
1
Dapatkah Anda mengomentari mengapa JSON Views (dalam jawaban saya) tidak sesuai dengan kebutuhan Anda?
Paul M
@pengguna: Ini mungkin solusi yang baik, saya membaca tentang itu dan mencoba.
Jonas
2
Perhatikan juga bahwa Anda dapat menggunakan @JsonSerialize (using = MySerializer.class) untuk menunjukkan serialisasi spesifik untuk properti Anda (field atau getter), jadi ini hanya digunakan untuk properti anggota dan BUKAN semua instance tipe.
StaxMan
8

Saya menulis contoh untuk Timestamp.classserialisasi / deserialisasi kustom , tetapi Anda dapat menggunakannya untuk apa pun yang Anda inginkan.

Saat membuat pemeta objek, lakukan sesuatu seperti ini:

public class JsonUtils {

    public static ObjectMapper objectMapper = null;

    static {
        objectMapper = new ObjectMapper();
        SimpleModule s = new SimpleModule();
        s.addSerializer(Timestamp.class, new TimestampSerializerTypeHandler());
        s.addDeserializer(Timestamp.class, new TimestampDeserializerTypeHandler());
        objectMapper.registerModule(s);
    };
}

misalnya di java eeAnda bisa menginisialisasi dengan ini:

import java.time.LocalDateTime;

import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Provider;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;

@Provider
public class JacksonConfig implements ContextResolver<ObjectMapper> {

    private final ObjectMapper objectMapper;

    public JacksonConfig() {
        objectMapper = new ObjectMapper();
        SimpleModule s = new SimpleModule();
        s.addSerializer(Timestamp.class, new TimestampSerializerTypeHandler());
        s.addDeserializer(Timestamp.class, new TimestampDeserializerTypeHandler());
        objectMapper.registerModule(s);
    };

    @Override
    public ObjectMapper getContext(Class<?> type) {
        return objectMapper;
    }
}

di mana serializer harus seperti ini:

import java.io.IOException;
import java.sql.Timestamp;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;

public class TimestampSerializerTypeHandler extends JsonSerializer<Timestamp> {

    @Override
    public void serialize(Timestamp value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
        String stringValue = value.toString();
        if(stringValue != null && !stringValue.isEmpty() && !stringValue.equals("null")) {
            jgen.writeString(stringValue);
        } else {
            jgen.writeNull();
        }
    }

    @Override
    public Class<Timestamp> handledType() {
        return Timestamp.class;
    }
}

dan deserializer sesuatu seperti ini:

import java.io.IOException;
import java.sql.Timestamp;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.SerializerProvider;

public class TimestampDeserializerTypeHandler extends JsonDeserializer<Timestamp> {

    @Override
    public Timestamp deserialize(JsonParser jp, DeserializationContext ds) throws IOException, JsonProcessingException {
        SqlTimestampConverter s = new SqlTimestampConverter();
        String value = jp.getValueAsString();
        if(value != null && !value.isEmpty() && !value.equals("null"))
            return (Timestamp) s.convert(Timestamp.class, value);
        return null;
    }

    @Override
    public Class<Timestamp> handledType() {
        return Timestamp.class;
    }
}
orang gila
sumber
7

Ini adalah pola perilaku yang saya perhatikan saat mencoba memahami serialisasi Jackson.

1) Asumsikan ada objek Classroom dan satu class Student. Saya telah membuat semuanya menjadi publik dan final untuk kemudahan.

public class Classroom {
    public final double double1 = 1234.5678;
    public final Double Double1 = 91011.1213;
    public final Student student1 = new Student();
}

public class Student {
    public final double double2 = 1920.2122;
    public final Double Double2 = 2324.2526;
}

2) Asumsikan bahwa ini adalah serializers yang kami gunakan untuk membuat serialisasi objek ke dalam JSON. WriteObjectField menggunakan serializer objek itu sendiri jika itu terdaftar dengan pemeta objek; jika tidak, maka serialisasi itu sebagai POJO. WriteNumberField secara eksklusif hanya menerima primitif sebagai argumen.

public class ClassroomSerializer extends StdSerializer<Classroom> {
    public ClassroomSerializer(Class<Classroom> t) {
        super(t);
    }

    @Override
    public void serialize(Classroom value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException {
        jgen.writeStartObject();
        jgen.writeObjectField("double1-Object", value.double1);
        jgen.writeNumberField("double1-Number", value.double1);
        jgen.writeObjectField("Double1-Object", value.Double1);
        jgen.writeNumberField("Double1-Number", value.Double1);
        jgen.writeObjectField("student1", value.student1);
        jgen.writeEndObject();
    }
}

public class StudentSerializer extends StdSerializer<Student> {
    public StudentSerializer(Class<Student> t) {
        super(t);
    }

    @Override
    public void serialize(Student value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException {
        jgen.writeStartObject();
        jgen.writeObjectField("double2-Object", value.double2);
        jgen.writeNumberField("double2-Number", value.double2);
        jgen.writeObjectField("Double2-Object", value.Double2);
        jgen.writeNumberField("Double2-Number", value.Double2);
        jgen.writeEndObject();
    }
}

3) Daftarkan hanya DoubleSerializer dengan pola keluaran DecimalFormat ###,##0.000, di SimpleModule dan keluarannya adalah:

{
  "double1" : 1234.5678,
  "Double1" : {
    "value" : "91,011.121"
  },
  "student1" : {
    "double2" : 1920.2122,
    "Double2" : {
      "value" : "2,324.253"
    }
  }
}

Anda dapat melihat bahwa serialisasi POJO membedakan antara ganda dan Ganda, menggunakan DoubleSerialzer untuk Ganda dan menggunakan format String biasa untuk ganda.

4) Daftarkan DoubleSerializer dan ClassroomSerializer, tanpa StudentSerializer. Kita berharap bahwa hasilnya adalah jika kita menulis sebuah double sebagai sebuah objek, ia berperilaku seperti Double, dan jika kita menulis sebuah Double sebagai sebuah angka, ia berperilaku seperti sebuah double. Variabel instance Student harus ditulis sebagai POJO dan mengikuti pola di atas karena tidak terdaftar.

{
  "double1-Object" : {
    "value" : "1,234.568"
  },
  "double1-Number" : 1234.5678,
  "Double1-Object" : {
    "value" : "91,011.121"
  },
  "Double1-Number" : 91011.1213,
  "student1" : {
    "double2" : 1920.2122,
    "Double2" : {
      "value" : "2,324.253"
    }
  }
}

5) Daftarkan semua pembuat serial. Outputnya adalah:

{
  "double1-Object" : {
    "value" : "1,234.568"
  },
  "double1-Number" : 1234.5678,
  "Double1-Object" : {
    "value" : "91,011.121"
  },
  "Double1-Number" : 91011.1213,
  "student1" : {
    "double2-Object" : {
      "value" : "1,920.212"
    },
    "double2-Number" : 1920.2122,
    "Double2-Object" : {
      "value" : "2,324.253"
    },
    "Double2-Number" : 2324.2526
  }
}

persis seperti yang diharapkan.

Catatan penting lainnya: Jika Anda memiliki beberapa serializer untuk kelas yang sama yang terdaftar dengan Modul yang sama, maka Modul akan memilih serializer untuk kelas tersebut yang paling baru ditambahkan ke daftar. Ini tidak boleh digunakan - ini membingungkan dan saya tidak yakin seberapa konsistennya ini

Moral: jika Anda ingin menyesuaikan serialisasi primitif di objek Anda, Anda harus menulis serializer Anda sendiri untuk objek tersebut. Anda tidak dapat mengandalkan serialisasi POJO Jackson.

laughing_man
sumber
Bagaimana Anda mendaftarkan ClassroomSerializer untuk menangani, misalnya kejadian di Classroom?
Trismegistos
5

Tampilan JSON Jackson mungkin merupakan cara yang lebih sederhana untuk memenuhi kebutuhan Anda, terutama jika Anda memiliki fleksibilitas dalam format JSON Anda.

Jika {"id":7, "itemNr":"TEST", "createdBy":{id:3}}merupakan representasi yang dapat diterima maka ini akan sangat mudah dicapai dengan sedikit kode.

Anda hanya perlu menganotasi bidang nama Pengguna sebagai bagian dari tampilan, dan menentukan tampilan yang berbeda dalam permintaan serialisasi Anda (bidang yang tidak diberi anotasi akan disertakan secara default)

Misalnya: Tentukan tampilan:

public class Views {
    public static class BasicView{}
    public static class CompleteUserView{}
}

Anotasi Pengguna:

public class User {
    public final int id;

    @JsonView(Views.CompleteUserView.class)
    public final String name;

    public User(int id, String name) {
        this.id = id;
        this.name = name;
    }
}

Dan serialisasi meminta tampilan yang tidak berisi bidang yang ingin Anda sembunyikan (bidang non-anotasi diserialkan secara default):

objectMapper.getSerializationConfig().withView(Views.BasicView.class);
Paul M
sumber
Saya merasa Tampilan Jackson JSON sulit digunakan, dan tidak bisa mendapatkan solusi yang baik untuk masalah ini.
Jonas
Jonas - Saya telah menambahkan contoh. Saya menemukan pandangan sebagai solusi yang sangat bagus untuk menserialisasikan objek yang sama dengan cara yang berbeda.
Paul M
Terima kasih untuk contoh yang bagus. Ini adalah solusi terbaik sejauh ini. Tetapi apakah tidak ada cara untuk mendapatkan createdBynilai selain sebagai objek?
Jonas
setSerializationView()tampaknya sudah usang jadi saya gunakan mapper.viewWriter(JacksonViews.ItemView.class).writeValue(writer, myItem);sebagai gantinya.
Jonas
Saya ragu menggunakan jsonviews. Solusi cepat & kotor yang saya gunakan sebelum menemukan tampilan adalah hanya menyalin properti yang saya minati, ke dalam Peta, dan kemudian membuat serial peta.
Paul M
5

Dalam kasus saya (Spring 3.2.4 dan Jackson 2.3.1), konfigurasi XML untuk serializer khusus:

<mvc:annotation-driven>
    <mvc:message-converters register-defaults="false">
        <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
            <property name="objectMapper">
                <bean class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean">
                    <property name="serializers">
                        <array>
                            <bean class="com.example.business.serializer.json.CustomObjectSerializer"/>
                        </array>
                    </property>
                </bean>
            </property>
        </bean>
    </mvc:message-converters>
</mvc:annotation-driven>

dengan cara yang tidak dapat dijelaskan ditimpa kembali ke default oleh sesuatu.

Ini berhasil untuk saya:

CustomObject.java

@JsonSerialize(using = CustomObjectSerializer.class)
public class CustomObject {

    private Long value;

    public Long getValue() {
        return value;
    }

    public void setValue(Long value) {
        this.value = value;
    }
}

CustomObjectSerializer.java

public class CustomObjectSerializer extends JsonSerializer<CustomObject> {

    @Override
    public void serialize(CustomObject value, JsonGenerator jgen,
        SerializerProvider provider) throws IOException,JsonProcessingException {
        jgen.writeStartObject();
        jgen.writeNumberField("y", value.getValue());
        jgen.writeEndObject();
    }

    @Override
    public Class<CustomObject> handledType() {
        return CustomObject.class;
    }
}

Tidak ada konfigurasi XML ( <mvc:message-converters>(...)</mvc:message-converters>) yang diperlukan dalam solusi saya.

pengguna11153
sumber
1

Jika satu-satunya persyaratan Anda dalam serializer kustom Anda adalah melewati namebidang serialisasi User, tandai sebagai sementara . Jackson tidak akan membuat serial atau deserialisasi sementara bidang .

[lihat juga: Mengapa Java memiliki kolom transien? ]

Mike G
sumber
Dimana saya menandainya? Di Userkelas-? Tapi saya akan membuat serialisasi semua objek pengguna juga. Misalnya pertama hanya membuat serial semua items(dengan hanya userIdsebagai referensi ke objek pengguna) dan kemudian membuat serial semua users. Dalam hal ini saya tidak bisa menandai fiels di User-class.
Jonas
Mengingat informasi baru ini, pendekatan ini tidak akan berhasil untuk Anda. Sepertinya Jackson sedang mencari informasi lebih lanjut untuk metode serializer khusus (handlingType () perlu diganti?)
Mike G
Ya, tetapi tidak ada batasan tentang handledType()metode dalam dokumentasi yang saya tautkan dan kapan Eclipse menghasilkan metode untuk menerapkan tidak handledType()dibuat, jadi saya bingung.
Jonas
Saya tidak yakin karena wiki yang Anda tautkan tidak mereferensikannya, tetapi dalam versi 1.5.1 ada handleType () dan pengecualiannya tampaknya mengeluh bahwa metode tersebut tidak ada atau tidak valid (kelas dasar mengembalikan null dari metode tersebut). jackson.codehaus.org/1.5.1/javadoc/org/codehaus/jackson/map/…
Mike G
1

Anda harus mengganti metode handlingType dan semuanya akan bekerja

@Override
public Class<Item> handledType()
{
  return Item.class;
}
Sergey Kukurudzyak
sumber
0

Masalah dalam kasus Anda adalah ItemSerializer kehilangan metode handlingType () yang perlu diganti dari JsonSerializer

    public class ItemSerializer extends JsonSerializer<Item> {

    @Override
    public void serialize(Item value, JsonGenerator jgen,
            SerializerProvider provider) throws IOException,
            JsonProcessingException {
        jgen.writeStartObject();
        jgen.writeNumberField("id", value.id);
        jgen.writeNumberField("itemNr", value.itemNr);
        jgen.writeNumberField("createdBy", value.user.id);
        jgen.writeEndObject();
    }

   @Override
   public Class<Item> handledType()
   {
    return Item.class;
   }
}

Oleh karena itu Anda mendapatkan kesalahan eksplisit yang ditanganiType () tidak ditentukan

Exception in thread "main" java.lang.IllegalArgumentException: JsonSerializer of type com.example.ItemSerializer does not define valid handledType() 

Semoga bisa membantu seseorang. Terima kasih telah membaca jawaban saya.

Sanjay Bharwani
sumber