Как использовать пользовательский сериализатор с Джексоном?
У меня есть два 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 с этим кодом из инструкции Джексона: пользовательские сериализаторы:
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 нет поддержки интерфейсов.
10 ответов
Как уже упоминалось, @JsonValue - хороший способ. Но если вы не возражаете против пользовательского сериализатора, нет необходимости писать один для Item, а скорее для User - если это так, это будет так просто:
public void serialize(Item value, JsonGenerator jgen,
SerializerProvider provider) throws IOException,
JsonProcessingException {
jgen.writeNumber(id);
}
Еще одна возможность заключается в реализации JsonSerializable
, в этом случае регистрация не требуется.
Что касается ошибки; это странно - вы, вероятно, хотите перейти на более позднюю версию. Но это также безопаснее продлить org.codehaus.jackson.map.ser.SerializerBase
поскольку он будет иметь стандартные реализации несущественных методов (т. е. все, кроме фактического вызова сериализации).
Вы можете положить @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);
}
}
Я тоже пытался это сделать, и в примере кода на веб-странице Джексона есть ошибка, которая не включает тип (.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);
К вашему сведению: вот ссылка на правильный пример кода: http://wiki.fasterxml.com/JacksonFeatureModules
Надеюсь это поможет!
Используйте @JsonValue:
public class User {
int id;
String name;
@JsonValue
public int getId() {
return id;
}
}
@JsonValue работает только с методами, поэтому вы должны добавить метод getId. Вы должны иметь возможность вообще пропустить свой пользовательский сериализатор.
Я написал пример для кастома Timestamp.class
сериализация / десериализация, но вы можете использовать его для чего угодно.
При создании объекта Mapper сделайте что-то вроде этого:
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;
}
}
Это модели поведения, которые я заметил, пытаясь понять сериализацию Джексона.
1) Предположим, есть объект Classroom и класс Student. Я сделал все публично и окончательно для удобства.
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 с шаблоном вывода DecimalFormat ###,##0.000
, в SimpleModule и вывод:
{
"double1" : 1234.5678,
"Double1" : {
"value" : "91,011.121"
},
"student1" : {
"double2" : 1920.2122,
"Double2" : {
"value" : "2,324.253"
}
}
}
Вы можете видеть, что сериализация POJO различает double и Double, используя DoubleSerialzer для Doubles и используя обычный формат String для double.
4) Зарегистрируйте DoubleSerializer и ClassroomSerializer без использования StudentSerializer. Мы ожидаем, что результат будет таким, что если мы пишем double как объект, он ведет себя как Double, а если мы пишем 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 Jackson.
JSON Views Джексона может быть более простым способом выполнения ваших требований, особенно если вы обладаете некоторой гибкостью в своем формате 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);
В моем случае (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>
) нужен в моем решении.
Проблема в вашем случае заключается в том, что в 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()
Надеюсь, это кому-то поможет. Спасибо, что прочитали мой ответ.
Если ваше единственное требование в пользовательском сериализаторе - пропустить сериализацию name
поле User
, отметьте это как переходный процесс. Джексон не будет сериализовывать или десериализовывать переходные поля.
[см. также: Почему в Java есть временные поля? ]
Вы должны переопределить метод handledType, и все будет работать
@Override
public Class<Item> handledType()
{
return Item.class;
}