Как мне использовать настраиваемый сериализатор с Джексоном?

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

Но я получаю такую ​​ошибку:

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 - хороший способ. Но если вы не возражаете против настраиваемого сериализатора, нет необходимости писать его для Item, а лучше для User - в таком случае это будет так просто:

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

Еще одна возможность - реализовать JsonSerializable, и в этом случае регистрация не требуется.

Что касается ошибки; это странно - вы, вероятно, захотите перейти на более позднюю версию. Но также безопаснее расширять, org.codehaus.jackson.map.ser.SerializerBaseпоскольку он будет иметь стандартные реализации несущественных методов (то есть все, кроме фактического вызова сериализации).

StaxMan
источник
При этом я получаю ту же ошибку: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
Я использую последнюю стабильную версию Jacskson, 1.8.5.
Jonas
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);
    }

}
Moesio
источник
1
Стоит отметить: использовать @JsonSerialize(contentUsing= ...)при аннотировании Коллекций (например @JsonSerialize(contentUsing= CustomDateSerializer.class) List<Date> dates)
coderatchet
33

Я тоже пытался сделать это, и в примере кода на веб-странице Джексона есть ошибка, из-за которой не удается включить type ( .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

PMhargis
источник
9

Используйте @JsonValue:

public class User {
    int id;
    String name;

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

@JsonValue работает только с методами, поэтому вы должны добавить метод getId. Вы должны иметь возможность вообще пропустить свой собственный сериализатор.

henrik_lundgren
источник
2
Я думаю, что это повлияет на все попытки сериализации пользователя, что затруднит раскрытие имени пользователя через JSON.
Paul M
Я не могу использовать это решение, потому что мне также нужно иметь возможность сериализовать все пользовательские объекты со всеми полями. И это решение нарушит эту сериализацию, поскольку будет включено только поле id. Нет ли возможности создать собственный серилизатор для Джексона, как для Джексона?
Jonas
1
Можете ли вы прокомментировать, почему представления JSON (в моем ответе) не соответствуют вашим потребностям?
Paul M
@user: Это может быть хорошее решение, я читаю об этом и пытаюсь.
Jonas
2
Также обратите внимание, что вы можете использовать @JsonSerialize (using = 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;
    }
}
Madx
источник
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 различает 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.

смеющийся человек
источник
Как вы регистрируете ClassroomSerializer для обработки, например, случаев Classroom?
Трисмегистос
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, и я не могу найти хорошего решения этой проблемы.
Jonas
Йонас - Я добавил пример. Я нашел view действительно хорошим решением для сериализации одного и того же объекта разными способами.
Paul M
Спасибо за хороший пример. На данный момент это лучшее решение. Но разве нет возможности получить createdByзначение вместо объекта?
Jonas
setSerializationView()кажется устаревшим, поэтому я использовал mapper.viewWriter(JacksonViews.ItemView.class).writeValue(writer, myItem);вместо этого.
Jonas
Я сомневаюсь в использовании 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;
    }
}

В <mvc:message-converters>(...)</mvc:message-converters>моем решении конфигурация XML ( ) не требуется.

user11153
источник
1

Если ваше единственное требование в пользовательском сериализаторе - пропустить сериализацию nameполя User, отметьте его как временное . Джексон не будет сериализовать или десериализовать переходные процессы поля.

[см. также: Почему в Java есть временные поля? ]

Майк Джи
источник
Где это отметить? В User-классе? Но я тоже буду сериализовать все пользовательские объекты. Например, сначала сериализуйте только все items(только userIdв качестве ссылки на пользовательский объект), а затем сериализуйте все users. В этом случае я не могу отметить поля в User-классе.
Jonas
В свете этой новой информации этот подход вам не подойдет. Похоже, Джексон ищет дополнительную информацию о настраиваемом сериализаторе (метод handledType () нужно переопределить?)
Майк Дж.
Да, но handledType()в документации, на которую я ссылаюсь, ничего не говорится о методе, и когда Eclipse генерирует методы для реализации, не handledType()генерируется, поэтому я запутался.
Jonas
Я не уверен, потому что вики, которую вы связали, не ссылается на него, но в версии 1.5.1 есть handledType (), и исключение, похоже, жалуется, что метод отсутствует или недействителен (базовый класс возвращает значение null из метода). jackson.codehaus.org/1.5.1/javadoc/org/codehaus/jackson/map/…
Майк Джи
1

Вам нужно переопределить метод handledType, и все будет работать

@Override
public Class<Item> handledType()
{
  return Item.class;
}
Сергей Кукурудзяк
источник
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() 

Надеюсь, это кому-то поможет. Спасибо, что прочитали мой ответ.

Санджай Бхарвани
источник