Wie verwende ich einen benutzerdefinierten Serializer mit Jackson?

111

Ich habe zwei Java-Klassen, die ich mit Jackson in JSON serialisieren möchte:

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

Ich möchte ein Element für diesen JSON serialisieren:

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

mit Benutzer serialisiert, um nur die einzuschließen id. Ich werde auch in der Lage sein, alle Benutzerobjekte für JSON zu serilisieren, wie:

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

Ich denke also, dass ich einen benutzerdefinierten Serializer schreiben Itemund damit versuchen muss:

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

}

Ich serialisiere den JSON mit diesem Code von 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();
}

Aber ich bekomme diesen Fehler:

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)

Wie kann ich mit Jackson einen benutzerdefinierten Serializer verwenden?


So würde ich es mit Gson machen:

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

Aber ich muss es jetzt mit Jackson machen, da Gson keine Unterstützung für Schnittstellen hat.

Jonas
quelle
Wie / wo haben Sie Jackson dazu gebracht, Ihren benutzerdefinierten Serializer für das zu verwenden Item? Ich habe ein Problem, bei dem meine Controller-Methode ein standardisiertes serialisiertes Objekt zurückgibt TypeA, aber für eine andere bestimmte Controller-Methode möchte ich es anders serialisieren. Wie würde das aussehen?
Don Cheadle
Ich habe einen Beitrag über das Schreiben eines benutzerdefinierten Serializers mit Jackson geschrieben , der für einige hilfreich sein kann.
Sam Berry

Antworten:

51

Wie bereits erwähnt, ist @JsonValue ein guter Weg. Wenn Ihnen ein benutzerdefinierter Serializer nichts ausmacht, müssen Sie keinen für Item, sondern einen für User schreiben. Wenn ja, ist dies so einfach wie:

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

Eine weitere Möglichkeit ist die Implementierung JsonSerializable. In diesem Fall ist keine Registrierung erforderlich.

Was den Fehler betrifft; Das ist komisch - Sie möchten wahrscheinlich auf eine spätere Version aktualisieren. Es ist jedoch auch sicherer zu erweitern, org.codehaus.jackson.map.ser.SerializerBaseda es Standardimplementierungen nicht wesentlicher Methoden enthält (dh alles außer dem tatsächlichen Serialisierungsaufruf).

StaxMan
quelle
Damit bekomme ich den gleichen Fehler: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
Ich verwende die neueste stabile Version von Jacskson, 1.8.5.
Jonas
4
Vielen Dank. Ich werde einen Blick darauf werfen ... Ah! Es ist eigentlich einfach (obwohl die Fehlermeldung nicht gut ist) - Sie müssen nur den Serializer mit einer anderen Methode registrieren, um die Klasse anzugeben, für die der Serializer bestimmt ist: Wenn nicht, muss er die Klasse von handleType () zurückgeben. Verwenden Sie also 'addSerializer', der JavaType oder Class als Argument verwendet und funktionieren sollte.
StaxMan
Was ist, wenn dies nicht ausgeführt wird?
Matej J
62

Sie können @JsonSerialize(using = CustomDateSerializer.class)jedes Datumsfeld des zu serialisierenden Objekts überschreiben.

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
quelle
1
Bemerkenswert: Verwendung @JsonSerialize(contentUsing= ...)beim Kommentieren von Sammlungen (z. B. @JsonSerialize(contentUsing= CustomDateSerializer.class) List<Date> dates)
Coderatchet
33

Ich habe es auch versucht, und der Beispielcode auf der Jackson-Webseite enthält einen Fehler, der den Typ nicht enthält (.class ) nicht in den Aufruf der addSerializer()Methode einbezieht. Dieser sollte folgendermaßen lauten:

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

Mit anderen Worten, dies sind die Zeilen, die simpleModuleden Serializer instanziieren und hinzufügen (wobei die vorherige falsche Zeile auskommentiert ist):

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

Zu Ihrer Information: Hier ist die Referenz für den richtigen Beispielcode: http://wiki.fasterxml.com/JacksonFeatureModules

pmhargis
quelle
9

Verwenden Sie @JsonValue:

public class User {
    int id;
    String name;

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

@JsonValue funktioniert nur mit Methoden, daher müssen Sie die Methode getId hinzufügen. Sie sollten in der Lage sein, Ihren benutzerdefinierten Serializer vollständig zu überspringen.

henrik_lundgren
quelle
2
Ich denke, dies wird sich auf alle Versuche auswirken, einen Benutzer zu serialisieren, was es schwierig macht, den Namen eines Benutzers jemals über JSON verfügbar zu machen.
Paul M
Ich kann diese Lösung nicht verwenden, da ich auch alle Benutzerobjekte mit allen Feldern serialisieren muss. Und diese Lösung wird diese Serialisierung aufheben, da nur das ID-Feld enthalten sein wird. Gibt es keine Möglichkeit, einen benutzerdefinierten Serilisator für Jackson wie für Gson zu erstellen?
Jonas
1
Können Sie kommentieren, warum JSON-Ansichten (in meiner Antwort) nicht Ihren Anforderungen entsprechen?
Paul M
@user: Es kann eine gute Lösung sein, ich lese darüber und versuche es.
Jonas
2
Beachten Sie auch, dass Sie @JsonSerialize (using = MySerializer.class) verwenden können, um eine bestimmte Serialisierung für Ihre Eigenschaft (Feld oder Getter) anzugeben, sodass diese nur für die Member-Eigenschaft und NICHT für alle Instanzen des Typs verwendet wird.
StaxMan
8

Ich habe ein Beispiel für eine benutzerdefinierte Timestamp.classSerialisierung / Deserialisierung geschrieben, aber Sie können es verwenden, was immer Sie wollen.

Gehen Sie beim Erstellen des Objekt-Mappers folgendermaßen vor:

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

Zum Beispiel java eekönnten Sie es folgendermaßen initialisieren:

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

wo der Serializer so etwas sein sollte:

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

und Deserializer so etwas:

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

Dies sind Verhaltensmuster, die mir beim Versuch aufgefallen sind, die Jackson-Serialisierung zu verstehen.

1) Angenommen, es gibt ein Objekt Klassenzimmer und eine Klasse Schüler. Ich habe alles öffentlich und endgültig gemacht.

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) Angenommen, dies sind die Serialisierer, die wir zum Serialisieren der Objekte in JSON verwenden. Das writeObjectField verwendet den eigenen Serializer des Objekts, wenn es beim Object Mapper registriert ist. Wenn nicht, wird es als POJO serialisiert. Das writeNumberField akzeptiert ausschließlich nur Grundelemente als Argumente.

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) Registrieren Sie nur einen DoubleSerializer mit DecimalFormat-Ausgabemuster ###,##0.000in SimpleModule und die Ausgabe lautet:

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

Sie können sehen, dass die POJO-Serialisierung zwischen double und Double unterscheidet, indem Sie den DoubleSerialzer für Doubles und ein reguläres String-Format für Doubles verwenden.

4) Registrieren Sie DoubleSerializer und ClassroomSerializer ohne den StudentSerializer. Wir erwarten, dass die Ausgabe so ist, dass sie sich wie ein Double verhält, wenn wir ein Double als Objekt schreiben, und wenn wir ein Double als Zahl schreiben, verhält es sich wie ein Double. Die Student-Instanzvariable sollte als POJO geschrieben werden und dem obigen Muster folgen, da sie nicht registriert wird.

{
  "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) Registrieren Sie alle Serializer. Die Ausgabe ist:

{
  "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
  }
}

genau wie erwartet.

Ein weiterer wichtiger Hinweis: Wenn Sie mehrere Serializer für dieselbe Klasse mit demselben Modul registriert haben, wählt das Modul den Serializer für die Klasse aus, die zuletzt zur Liste hinzugefügt wurde. Dies sollte nicht verwendet werden - es ist verwirrend und ich bin nicht sicher, wie konsistent dies ist

Moral: Wenn Sie die Serialisierung von Grundelementen in Ihrem Objekt anpassen möchten, müssen Sie Ihren eigenen Serializer für das Objekt schreiben. Sie können sich nicht auf die POJO Jackson-Serialisierung verlassen.

lachender Mann
quelle
Wie registrieren Sie ClassroomSerializer, um z. B. Vorkommen von Classroom zu behandeln?
Trismegistos
5

Jacksons JSON-Ansichten sind möglicherweise eine einfachere Möglichkeit, Ihre Anforderungen zu erfüllen, insbesondere wenn Sie in Ihrem JSON-Format eine gewisse Flexibilität haben.

Wenn dies {"id":7, "itemNr":"TEST", "createdBy":{id:3}}eine akzeptable Darstellung ist, ist dies mit sehr wenig Code sehr einfach zu erreichen.

Sie würden lediglich das Namensfeld des Benutzers als Teil einer Ansicht mit Anmerkungen versehen und eine andere Ansicht in Ihrer Serialisierungsanforderung angeben (die nicht mit Anmerkungen versehenen Felder wären standardmäßig enthalten).

Zum Beispiel: Definieren Sie die Ansichten:

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

Kommentieren Sie den Benutzer:

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

Und serialisieren Sie das Anfordern einer Ansicht, die nicht das Feld enthält, das Sie ausblenden möchten (nicht kommentierte Felder werden standardmäßig serialisiert):

objectMapper.getSerializationConfig().withView(Views.BasicView.class);
Paul M.
quelle
Ich finde die Jackson JSON-Ansichten schwer zu verwenden und kann keine gute Lösung für dieses Problem finden.
Jonas
Jonas - Ich habe ein Beispiel hinzugefügt. Ich fand Ansichten eine wirklich gute Lösung, um dasselbe Objekt auf unterschiedliche Weise zu serialisieren.
Paul M
Danke für ein gutes Beispiel. Dies ist die bisher beste Lösung. Aber gibt es keine Möglichkeit, createdByals Wert statt als Objekt zu erhalten?
Jonas
setSerializationView()scheinen veraltet zu sein, also habe ich mapper.viewWriter(JacksonViews.ItemView.class).writeValue(writer, myItem);stattdessen verwendet.
Jonas
Ich bezweifle es mit jsonviews. Eine schnelle und schmutzige Lösung, die ich vor dem Erkennen von Ansichten verwendet habe, bestand darin, die Eigenschaften, an denen ich interessiert war, in eine Karte zu kopieren und dann die Karte zu serialisieren.
Paul M
5

In meinem Fall (Spring 3.2.4 und Jackson 2.3.1), XML-Konfiguration für benutzerdefinierten Serializer:

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

wurde auf unerklärliche Weise von etwas auf Standard zurückgeschrieben.

Das hat bei mir funktioniert:

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

In <mvc:message-converters>(...)</mvc:message-converters>meiner Lösung ist keine XML-Konfiguration ( ) erforderlich.

user11153
quelle
1

Wenn Ihre einzige Anforderung in Ihrem benutzerdefinierten Serializer darin besteht, das Serialisieren des nameFelds von zu überspringen User, markieren Sie es als vorübergehend . Jackson wird transient nicht serialisieren oder deserialisieren Felder .

[siehe auch: Warum hat Java Übergangsfelder? ]]

Mike G.
quelle
Wo markiere ich es? In der UserKlasse? Aber ich werde auch alle Benutzerobjekte serialisieren. ZB zuerst nur alle serialisieren items(nur userIdals Referenz auf das Benutzerobjekt) und dann alle serialisieren users. In diesem Fall kann ich die Felder in der UserKlasse nicht markieren .
Jonas
Angesichts dieser neuen Informationen funktioniert dieser Ansatz für Sie nicht. Es sieht so aus, als ob Jackson nach weiteren Informationen für den benutzerdefinierten Serializer sucht (handleType () -Methode muss überschrieben werden?)
Mike G
Ja, aber es gibt nichts über die handledType()Methode in der Dokumentation, mit der ich verlinkt habe, und wenn Eclipse die Methoden zur Implementierung von no handledType()generiert, wird sie generiert, daher bin ich verwirrt.
Jonas
Ich bin mir nicht sicher, weil das von Ihnen verlinkte Wiki nicht darauf verweist, aber in Version 1.5.1 gibt es einen handleType () und die Ausnahme scheint zu beanstanden, dass die Methode fehlt oder ungültig ist (Basisklasse gibt null von der Methode zurück). jackson.codehaus.org/1.5.1/javadoc/org/codehaus/jackson/map/…
Mike G
1

Sie müssen die Methode handleType überschreiben und alles wird funktionieren

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

Das Problem in Ihrem Fall ist, dass dem ItemSerializer die Methode handleType () fehlt, die von JsonSerializer überschrieben werden muss

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

Daher erhalten Sie den expliziten Fehler, dass handleType () nicht definiert ist

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

Hoffe es hilft jemandem. Danke, dass Sie meine Antwort gelesen haben.

Sanjay Bharwani
quelle