Jak używać niestandardowego serializatora z Jacksonem?

111

Mam dwie klasy Java, które chcę serializować do JSON przy użyciu Jacksona:

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

Chcę serializować element do tego JSON:

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

z serializacją użytkownika, aby zawierał tylko id. Będę również mógł serilizować wszystkie obiekty użytkownika do JSON, takie jak:

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

Więc myślę, że muszę napisać niestandardowy serializator Itemi wypróbować z tym:

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

}

Serializuję JSON za pomocą tego kodu z 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();
}

Ale pojawia się ten błąd:

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)

Jak mogę używać niestandardowego serializatora z Jacksonem?


Tak bym to zrobił z Gsonem:

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

Ale muszę to teraz zrobić z Jacksonem, ponieważ Gson nie obsługuje interfejsów.

Jonas
źródło
jak / gdzie udało Ci się zdobyć Jacksona, aby użył Twojego niestandardowego Serializera Item? Mam problem polegający na tym, że moja metoda kontrolera zwraca standardowy serializowany obiekt TypeA, ale dla innej określonej metody kontrolera chcę serializować go inaczej. Jakby to wyglądało?
Don Cheadle
Napisałem post o tym, jak napisać niestandardowy serializator z Jacksonem, który może być pomocny dla niektórych.
Sam Berry

Odpowiedzi:

51

Jak wspomniano, @JsonValue to dobry sposób. Ale jeśli nie masz nic przeciwko niestandardowemu serializatorowi, nie musisz pisać go dla elementu, ale raczej dla użytkownika - jeśli tak, byłoby to tak proste, jak:

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

Jeszcze inną możliwością jest wdrożenie JsonSerializable, w którym to przypadku nie jest wymagana rejestracja.

Co do błędu; to dziwne - prawdopodobnie chcesz uaktualnić do nowszej wersji. Ale bezpieczniej jest też rozszerzyć, org.codehaus.jackson.map.ser.SerializerBaseponieważ będzie miał standardowe implementacje metod nieistotnych (tj. Wszystko oprócz rzeczywistego wywołania serializacji).

StaxMan
źródło
Z tym otrzymuję ten sam błąd: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
Używam najnowszej stabilnej wersji Jacskson, 1.8.5.
Jonas
4
Dzięki. Rzucę okiem ... Ach! W rzeczywistości jest to proste (chociaż komunikat o błędzie nie jest dobry) - wystarczy zarejestrować serializator inną metodą, aby określić klasę, dla której serializator jest przeznaczony: jeśli nie, musi zwrócić klasę z metody handledType (). Więc użyj 'addSerializer', który przyjmuje JavaType lub Class jako argument i powinno działać.
StaxMan
A co, jeśli to nie jest uruchamiane?
Matej J
62

Możesz umieścić @JsonSerialize(using = CustomDateSerializer.class)dowolne pole daty obiektu do serializacji.

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
źródło
1
warte odnotowania: użyj @JsonSerialize(contentUsing= ...)podczas adnotacji kolekcji (np. @JsonSerialize(contentUsing= CustomDateSerializer.class) List<Date> dates)
coderatchet
33

Ja też próbowałem to zrobić, ale w przykładowym kodzie na stronie internetowej Jacksona jest błąd, który nie zawiera typu ( .class) w wywołaniu addSerializer()metody, co powinno wyglądać następująco:

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

Innymi słowy, są to wiersze, które tworzą instancję simpleModulei dodają serializator (z uprzednią niepoprawną linią wykomentowaną):

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: Oto odniesienie do prawidłowego przykładowego kodu: http://wiki.fasterxml.com/JacksonFeatureModules

pmhargis
źródło
9

Użyj @JsonValue:

public class User {
    int id;
    String name;

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

@JsonValue działa tylko na metodach, więc musisz dodać metodę getId. Powinieneś móc całkowicie pominąć swój niestandardowy serializator.

henrik_lundgren
źródło
2
Myślę, że wpłynie to na wszystkie próby serializacji użytkownika, utrudniając kiedykolwiek ujawnienie nazwy użytkownika przez JSON.
Paul M
Nie mogę skorzystać z tego rozwiązania, ponieważ potrzebuję również możliwości serializacji wszystkich obiektów użytkownika ze wszystkimi polami. To rozwiązanie przerwie tę serializację, ponieważ zostanie uwzględnione tylko pole id. Czy nie ma sposobu na stworzenie niestandardowego serializatora dla Jacksona, tak jak w przypadku Gson?
Jonas
1
Czy możesz skomentować, dlaczego widoki JSON (w mojej odpowiedzi) nie odpowiadają Twoim potrzebom?
Paul M
@user: To może być dobre rozwiązanie, czytam o tym i próbuję.
Jonas
2
Zauważ również, że możesz użyć @JsonSerialize (using = MySerializer.class) do wskazania określonej serializacji dla swojej właściwości (pola lub metody pobierającej), więc jest ona używana tylko dla właściwości elementu członkowskiego, a NIE dla wszystkich wystąpień typu.
StaxMan
8

Napisałem przykład niestandardowej Timestamp.classserializacji / deserializacji, ale możesz go używać do wszystkiego, co chcesz.

Podczas tworzenia mapowania obiektów zrób coś takiego:

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

na przykład w java eemożesz zainicjować go tym:

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

gdzie serializator powinien wyglądać mniej więcej tak:

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

i deserializator coś takiego:

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

Oto wzorce zachowania, które zauważyłem, próbując zrozumieć serializację Jacksona.

1) Załóżmy, że istnieje obiekt Klasa i klasa Uczeń. Dla ułatwienia wszystko upubliczniłem i ostatecznie

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) Załóżmy, że są to serializatory, których używamy do serializacji obiektów do formatu JSON. WriteObjectField używa własnego serializatora obiektu, jeśli jest zarejestrowany w programie odwzorowującym obiekty; jeśli nie, to serializuje go jako POJO. WriteNumberField przyjmuje wyłącznie prymitywy jako argumenty.

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) Zarejestruj tylko DoubleSerializer z wzorcem wyjściowym DecimalFormat ###,##0.000, w SimpleModule, a wynik to:

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

Możesz zobaczyć, że serializacja POJO rozróżnia double i Double, używając DoubleSerialzer for Doubles i używając zwykłego formatu String dla podwójnych.

4) Zarejestruj DoubleSerializer i ClassroomSerializer, bez StudentSerializer. Spodziewamy się, że wynik jest taki, że jeśli napiszemy double jako obiekt, zachowuje się jak Double, a jeśli napiszemy Double jako liczbę, zachowuje się jak double. Zmienna instancji Student powinna być zapisana jako POJO i postępować zgodnie z powyższym wzorcem, ponieważ nie jest rejestrowana.

{
  "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) Zarejestruj wszystkie serializatory. Wynik to:

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

dokładnie tak, jak oczekiwano.

Kolejna ważna uwaga: jeśli masz wiele serializatorów dla tej samej klasy zarejestrowanych w tym samym module, moduł wybierze serializator dla tej klasy, która została ostatnio dodana do listy. Nie należy tego używać - jest to mylące i nie jestem pewien, jak spójne jest to

Morał: jeśli chcesz dostosować serializację prymitywów w swoim obiekcie, musisz napisać własny serializator dla obiektu. Nie możesz polegać na serializacji POJO Jackson.

laughing_man
źródło
Jak zarejestrować ClassroomSerializer, aby obsłużyć np. Zdarzenia Classroom?
Trismegistos
5

Widoki JSON Jacksona mogą być prostszym sposobem spełnienia twoich wymagań, zwłaszcza jeśli masz pewną elastyczność w swoim formacie JSON.

Jeśli {"id":7, "itemNr":"TEST", "createdBy":{id:3}}jest to akceptowalna reprezentacja, będzie to bardzo łatwe do osiągnięcia przy bardzo małej ilości kodu.

Po prostu dodasz adnotację do pola nazwy użytkownika jako część widoku i określ inny widok w żądaniu serializacji (pola bez adnotacji zostaną uwzględnione domyślnie)

Na przykład: Zdefiniuj widoki:

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

Opisz użytkownika:

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

I serializowanie żądające widoku, który nie zawiera pola, które chcesz ukryć (pola bez adnotacji są domyślnie serializowane):

objectMapper.getSerializationConfig().withView(Views.BasicView.class);
Paul M.
źródło
Uważam, że Jackson JSON Views jest trudny w użyciu i nie mogę znaleźć dobrego rozwiązania tego problemu.
Jonas
Jonas - dodałem przykład. Uważam, że widoki są naprawdę fajnym rozwiązaniem do serializacji tego samego obiektu na różne sposoby.
Paul M
Dzięki za dobry przykład. Jak dotąd to najlepsze rozwiązanie. Ale czy nie ma sposobu, aby uzyskać createdByjako wartość zamiast jako obiekt?
Jonas
setSerializationView()wydają się być przestarzałe, więc mapper.viewWriter(JacksonViews.ItemView.class).writeValue(writer, myItem);zamiast tego użyłem .
Jonas
Wątpię w to przy użyciu jsonviews. Szybkim i brudnym rozwiązaniem, którego użyłem przed odkryciem widoków, było po prostu skopiowanie właściwości, które mnie interesowały, do mapy, a następnie serializacja mapy.
Paul M
5

W moim przypadku (Spring 3.2.4 i Jackson 2.3.1), konfiguracja XML dla niestandardowego serializatora:

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

został w niewyjaśniony sposób przez coś nadpisany z powrotem do wartości domyślnej.

To zadziałało dla mnie:

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

<mvc:message-converters>(...)</mvc:message-converters>W moim rozwiązaniu nie jest wymagana konfiguracja XML ( ).

user11153
źródło
1

Jeśli jedynym wymaganiem w niestandardowym serializatorze jest pominięcie serializacji namepola User, oznacz je jako przejściowe . Jackson nie będzie serializować ani deserializować pól przejściowych .

[zobacz także: Dlaczego Java ma pola przejściowe? ]

Mike G.
źródło
Gdzie mam to zaznaczyć? W klasie User? Ale zserializuję również wszystkie obiekty użytkowników. Np. Najpierw serializuj wszystko items(tylko userIdjako odniesienie do obiektu użytkownika), a następnie serializuj wszystko users. W tym przypadku nie mogę zaznaczyć pól w User-class.
Jonas
W świetle tych nowych informacji to podejście nie zadziała. Wygląda na to, że Jackson szuka więcej informacji na temat niestandardowego serializatora (metoda handledType () wymaga zastąpienia?)
Mike G
Tak, ale nie ma nic o handledType()metodzie w dokumentacji, z którą się łączyłem, a kiedy Eclipse generuje metody do implementacji nie handledType()jest generowane, więc jestem zdezorientowany.
Jonas
Nie jestem pewien, ponieważ połączona witryna wiki nie odwołuje się do niej, ale w wersji 1.5.1 występuje metoda handledType (), a wyjątek wydaje się narzekać, że brakuje metody lub jest ona nieprawidłowa (klasa bazowa zwraca wartość null z metody). jackson.codehaus.org/1.5.1/javadoc/org/codehaus/jackson/map/…
Mike G
1

Musisz nadpisać metodę handledType i wszystko będzie działać

@Override
public Class<Item> handledType()
{
  return Item.class;
}
Sergey Kukurudzyak
źródło
0

Problem w Twoim przypadku polega na tym, że w ItemSerializer brakuje metody handledType (), która musi zostać zastąpiona przez 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;
   }
}

W związku z tym otrzymujesz jawny błąd, że nie zdefiniowano metody handledType ()

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

Mam nadzieję, że to komuś pomoże. Dziękuję za przeczytanie mojej odpowiedzi.

Sanjay Bharwani
źródło