Як використовувати користувальницький серіалізатор з Jackson?


111

У мене є два класи Java, які я хочу серіалізувати в JSON за допомогою Джексона:

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

Я хочу серіалізувати предмет до цього JSON:

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

із серіалізацією Користувача, щоб включати лише id. Я також зможу серіалізувати всі об’єкти користувача на JSON:

{"id":3, "name": "Jonas", "email": "jonas@example.com"}

Тому я здогадуюсь, що мені потрібно написати спеціальний серіалізатор для цього Itemі спробував це:

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

}

Я серіалізую JSON з цим кодом від Jackson How-to: Спеціальні серіалізатори :

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

Але я отримую цю помилку:

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)

Як я можу використовувати спеціальний серіалізатор з Джексоном?


Ось як я це зробив би з 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);

Але мені потрібно це зробити з Джексоном зараз, оскільки у Gson немає підтримки інтерфейсів.


як / де ви змусили Джексона використовувати свій власний серіалізатор для Item? У мене виникає проблема, коли мій метод контролера повертає стандартний серіалізований об'єкт TypeA, але для іншого конкретного методу контролера я хочу його серіалізувати інакше. Як би це виглядало?
Дон Чедл

Я написав пост про те, як написати спеціальний серіалізатор з Джексоном, який може бути корисним для деяких.
Сем Беррі

Відповіді:


51

Як уже згадувалося, @JsonValue - це хороший спосіб. Але якщо ви не заперечуєте проти користувацького серіалізатора, не потрібно писати його для елемента, а для користувача - якщо так, то це буде так просто:

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

Ще одна можливість - це реалізувати JsonSerializable, і тоді реєстрація не потрібна.

Щодо помилки; це дивно - ви, мабуть, хочете перейти на більш пізню версію. Але це також безпечніше розширити, org.codehaus.jackson.map.ser.SerializerBaseоскільки він матиме стандартні реалізації несуттєвих методів (тобто всього, крім фактичного виклику серіалізації).


З цим я отримую ту саму помилку: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)
Йонас

Я використовую останню стабільну версію Jacskson, 1.8.5.
Йонас

4
Дякую. Подивлюся ... Ах! Це насправді просто (хоча повідомлення про помилку не дуже добре) - вам просто потрібно зареєструвати серіалізатор іншим методом, вказати клас, для якого використовується серіалізатор: якщо ні, він повинен повернути клас з handledType (). Тому використовуйте 'addSerializer', який бере аргумент JavaType або Class, і він повинен працювати.
StaxMan

Що робити, якщо це не виконується?
Matej J

62

Ви можете розмістити @JsonSerialize(using = CustomDateSerializer.class)над будь-яким полем дати об'єкт, який підлягає серіалізації.

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

}

1
Варто зауважити: використовувати, @JsonSerialize(contentUsing= ...)коли @JsonSerialize(contentUsing= CustomDateSerializer.class) List<Date> dates
коментувати

33

Я також спробував це зробити, і в прикладі коду на веб-сторінці Джексона є помилка, яка не включає тип ( .class) у виклик addSerializer()методу, який повинен читати так:

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

Іншими словами, це рядки, які створюють simpleModuleі додають серіалізатор (з попереднім коментованим неправильним рядком):

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: Ось посилання на правильний код прикладу: http://wiki.fasterxml.com/JacksonFeatureModules


9

Використовуйте @JsonValue:

public class User {
    int id;
    String name;

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

@JsonValue працює лише над методами, тому ви повинні додати метод getId. Ви повинні мати можливість пропустити власний серіалізатор взагалі.


2
Я думаю, що це вплине на всі спроби серіалізувати Користувача, ускладнюючи відкриття імені Користувача через JSON.
Пол М

Я не можу використовувати це рішення, тому що мені також потрібно мати можливість серіалізувати всі об’єкти користувача з усіма полями. І це рішення порушить цю серіалізацію, оскільки буде включено лише поле id. Хіба немає способу створити спеціальний серіалізатор для Джексона, як це для Gson?
Йонас

1
Чи можете ви прокоментувати, чому JSON Views (на мою відповідь) не відповідає вашим потребам?
Пол М

@user: Це може бути хорошим рішенням, я читаю про це і намагаюся.
Йонас

2
Зауважте також, що ви можете використовувати @JsonSerialize (використовуючи = MySerializer.class) для вказівки конкретної серіалізації для вашого ресурсу (поля чи геттера), тому він використовується лише для власності учасника, а НЕ для всіх типів типу.
StaxMan

8

Я написав приклад для спеціальної Timestamp.classсеріалізації / десеріалізації, але ви можете використовувати її для чого завгодно.

Створюючи картографічний об'єкт, зробіть щось подібне:

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

наприклад, java eeви можете ініціалізувати це з цим:

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

де серіалізатор має бути приблизно таким:

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

і десеріалізатор щось подібне:

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

7

Це моделі поведінки, які я помітив, намагаючись зрозуміти серіалізацію Джексона.

1) Припустимо, що є об’єкт Класна кімната та клас Учень. Я зробив все загальнодоступним та остаточним.

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) Припустимо, що це серіалізатори, які ми використовуємо для серіалізації об'єктів у JSON. WriteObjectField використовує власний серіалізатор об'єкта, якщо він зареєстрований у картографічному об'єкті; якщо ні, то це серіалізує це як POJO. WriteNumberField виключно приймає примітиви як аргументи.

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) Зареєструйте лише DoubleSerializer з вихідним малюнком ###,##0.000DecimalFormat у SimpleModule, а вихід:

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

Ви можете бачити, що серіалізація POJO відрізняє подвійну та подвійну, використовуючи DoubleSerialzer для парних пар і використовуючи звичайний формат String для парних пар.

4) Зареєструйте DoubleSerializer та ClassroomSerializer, без StudentSerializer. Ми очікуємо, що вихід такий, що якщо ми записуємо подвійний як об'єкт, він поводиться як Double, а якщо ми пишемо Double як число, він поводиться як подвійний. Змінна екземпляра Student повинна бути записана як POJO і слідувати зразку, описаному вище, оскільки вона не реєструється.

{
  "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) Зареєструйте всі серіалізатори. Вихід:

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

точно так, як очікувалося.

Ще одне важливе зауваження: Якщо у вас є кілька зареєстрованих серіалізаторів для одного класу, зареєстрованих у тому ж Модулі, то модуль вибере серіалізатор для того класу, який останнім часом додається до списку. Це не слід використовувати - це заплутано, і я не впевнений, наскільки це послідовно

Мораль: якщо ви хочете налаштувати серіалізацію примітивів у вашому об'єкті, ви повинні написати власний серіалізатор для об'єкта. Ви не можете покластися на серіалізацію POJO Джексона.


Як зареєструвати ClassroomSerializer для обробки, наприклад, виникнення класу?
Trismegistos

5

Перегляди Джексона JSON можуть бути більш простим способом досягнення ваших вимог, особливо якщо у вас є певна гнучкість у форматі JSON.

Якщо {"id":7, "itemNr":"TEST", "createdBy":{id:3}}це прийнятне представлення, то це буде дуже легко досягти з дуже невеликим кодом.

Ви просто зазначаєте поле імені Користувача як частину перегляду та вказуєте інший вигляд у своєму запиті на серіалізацію (поля, що не позначаються, буде включено за замовчуванням)

Наприклад: Визначте перегляди:

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

Повідомлення користувача:

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

І серійний запит перегляду, який не містить поля, яке ви хочете приховати (поля, що не позначаються, серіалізуються за замовчуванням):

objectMapper.getSerializationConfig().withView(Views.BasicView.class);

Я вважаю, що Jackson JSON Views важкий у використанні, і не можу отримати хороше рішення для цієї проблеми.
Йонас

Йонас - я додав приклад. Я вважав погляди справді приємним рішенням для серіалізації одного і того ж об'єкта різними способами.
Пол М

Дякую за хороший приклад. Це найкраще рішення поки що. Але хіба немає способу отримати createdByяк значення замість як об’єкт?
Йонас

setSerializationView()здається застарілим, тому я використовував mapper.viewWriter(JacksonViews.ItemView.class).writeValue(writer, myItem);замість цього.
Йонас

Я сумніваюся в цьому за допомогою jsonviews. Швидке і брудне рішення, яке я використовував перед тим, як виявити види, полягав у тому, щоб скопіювати цікаві мені властивості на карту, а потім серіалізувати карту.
Пол М

5

У моєму випадку (Spring 3.2.4 та Jackson 2.3.1), конфігурація XML для користувацького серіалізатора:

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

було незрозумілим способом щось перезаписане назад до дефолту.

Це працювало для мене:

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

Конфігурація XML ( <mvc:message-converters>(...)</mvc:message-converters>) не потрібна в моєму рішенні.


1

Якщо вашою єдиною вимогою у вашому користувальницькому серіалізаторі є пропуск серіалізації nameполя User, позначте його як минуще . Джексон не буде серіалізувати або десеріалізувати перехідні поля.

[див. також: Чому у Java є перехідні поля? ]


Де я це маркую? В User-класі? Але я також серіалізую всі об’єкти користувача. Наприклад, спершу лише серіалізуйте всі items(лише userIdяк посилання на об’єкт користувача), а потім серіалізуйте всі users. У цьому випадку я не можу позначити знаки в -класі User.
Йонас

Зважаючи на нову інформацію, цей підхід не буде працювати для вас. Схоже, Джексон шукає додаткову інформацію для користувальницького серіалізатора (метод handledType () потребує переоцінки?)
Майк Г

Так, але handledType()в документації, до якої я пов’язаний , немає нічого спільного з методом, і коли Eclipse генерує методи для реалізації, не handledType()створюється, тому я плутаюся.
Йонас

Я не впевнений, тому що вікі, на які ви пов’язані, не посилаються на це, але у версії 1.5.1 є handledType (), і виняток, здається, скаржиться на те, що метод відсутній або недійсний (базовий клас повертає нуль з методу). jackson.codehaus.org/1.5.1/javadoc/org/codehaus/jackson/map/…
Майк Г


0

Проблема у вашому випадку полягає в тому, що у ItemSerializer відсутній метод handledType (), який потрібно перекрити з 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;
   }
}

Отже, ви отримуєте явну помилку, що handledType () не визначено

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

Сподіваюся, це комусь допоможе. Дякую за прочитання моєї відповіді.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.