Напишите две записи JSON из одного поля Java

Проблема:

У меня есть группа DTOs со структурой вроде этого:

public class Foobar {
    private String name;
    private Timestamp time1;
    private Timestamp time2;
    private int value;
}

Мне нужно сериализовать значения Timestamps до двух разных (один раз .toString() и один раз отформатирован в соответствии со стандартом ISO), чтобы быть обратно совместимым со старым API, а также поддерживать с сегодняшнего дня - достойный формат времени.

Итак, вывод JSON для Foobar должен выглядеть так:

{
    "name":"<some name>",
    "time1":"<some non standard time>",
    "iso_time1":"<ISO formatted time>",
    "time2":"<some non standard time>",
    "iso_time2":"<ISO formatted time>",
    "value":<some number>
}

Я ограничен Gson из-за существующего кода.

Вопрос:

Возможно ли это сделать общим способом, который будет работать для всех моих DTOs без изменения DTOs?

Я хочу, чтобы не писать TypeAdapter/Serializer/new DTO для каждого из моих существующих DTO.

Что я пробовал

TypeAdapter

Я уже пытался сделать это с помощью TypeAdapter и TypeAdapterFactory, но мне нужно имя поля этого класса, чтобы выделить две метки времени.

write(...) метод TypeAdapter, иллюстрирующий проблему, с которой я столкнулся (T extends Timestamp):

@Override
public void write(final JsonWriter out, final T value) throws IOException {
    out.value(value.toString());
    out.name(TIMESTAMP_ISO_PREFIX + fieldName).value(toISOFormat(value));
}

Проблема здесь в том, что я не нашел способа получить имя поля. Я попытался получить его с помощью TypeAdapterFactory, но factory также не знает имя поля.

JsonSerializer

Я также пытался сделать это с помощью JsonSerializer, но невозможно вернуть два элемента JSON, и возврат JsonObject нарушит существующий API.

Ответ 1

Подход 1: используйте JsonSerialiser

Вы можете создать JsonSerialiser для своих объектов (т.е. на один уровень выше, чем Timestamp) и использовать его для добавления дополнительных полей по мере необходимости:

/**
 * Appends extra fields containing ISO formatted times for all Timestamp properties of an Object.
 */
class TimestampSerializer implements JsonSerializer<Object> {
    private Gson gson = new GsonBuilder().create();

    @Override
    public JsonElement serialize(Object src, Type typeOfSrc, JsonSerializationContext context) {
        JsonElement tree = gson.toJsonTree(src);
        if (tree instanceof JsonObject) {
            appendIsoTimestamps(src, (JsonObject) tree);
        }
        return tree;
    }

    private JsonObject appendIsoTimestamps(Object src, JsonObject object) {
        try {
            PropertyDescriptor[] descriptors = Introspector.getBeanInfo(src.getClass()).getPropertyDescriptors();
            for (PropertyDescriptor descriptor : descriptors) {
                if (descriptor.getPropertyType().equals(Timestamp.class)) {
                    Timestamp ts = (Timestamp) descriptor.getReadMethod().invoke(src);
                    object.addProperty("iso_" + descriptor.getName(), ts.toInstant().toString());
                }
            }
            return object;
        } catch (IllegalAccessException | InvocationTargetException | IntrospectionException e) {
            throw new JsonIOException(e);
        }
    }

Пример использования:

public class GsonSerialiserTest {
    public static void main(String[] args) {
        GsonBuilder builder = new GsonBuilder();
        builder.registerTypeAdapter(Foobar.class, new TimestampSerializer());
        Gson gson = builder.create();
        Foobar baz = new Foobar("baz", 1, new Timestamp(System.currentTimeMillis()));
        System.out.println(gson.toJson(baz));
    }
}

Некоторые примечания:

  • В этом примере пользовательский конструктор java bean находит свойства Timestamp. Он опирается на наличие геттерных методов. Если у вас нет геттеров, вам придется использовать какой-либо другой метод для чтения ваших свойств метки времени.
  • Сериализатор делегирует другому строителю gson (он не может вызывать тот, что находится в JsonSerializationContext, или он в конечном итоге вызывает себя рекурсивно). Если ваша существующая сериализация зависит от другого инструментария в построителе, вам придется подключить отдельный строитель и передать его в сериализатор.

Если вы хотите сделать это для всех объектов, которые вы сериализуете, зарегистрируйте адаптер для всей иерархии Object:

builder.registerTypeHierarchyAdapter(Object.class, typeAdapter);

Если вы хотите изменить подмножество DTO, вы можете зарегистрировать их динамически. Библиотека Reflections упрощает:

TimestampSerializer typeAdapter = new TimestampSerializer();

Reflections reflections = new Reflections(new ConfigurationBuilder()
    .setScanners(new SubTypesScanner(false))
    .setUrls(ClasspathHelper.forClassLoader(ClasspathHelper.contextClassLoader()))
    .filterInputsBy(new FilterBuilder().includePackage("com.package.dto", "com.package.other")));

Set<Class<?>> classes = reflections.getSubTypesOf(Object.class);

for (Class<?> type : classes) {
    builder.registerTypeAdapter(type, typeAdapter);
}

Приведенный выше пример регистрирует все в именованных пакетах. Если ваши DTO соответствуют шаблону именования или реализуют общий интерфейс/имеют общую аннотацию, вы можете еще больше ограничить регистрацию.

Подход 2: Зарегистрируйте a TypeAdapterFactory

TypeAdapters работают на уровне читателя/писателя и требуют немного больше работы для реализации, но они дают вам больше контроля.

Регистрация TypeAdapterFactory с помощью построителя позволит вам контролировать, какие типы редактировать. В этом примере адаптер применяется ко всем типам:

public static void main(String[] args) {
    GsonBuilder builder = new GsonBuilder();

    builder.registerTypeAdapterFactory(new TypeAdapterFactory() {
        public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
            // Return null here if you don't want to handle the type.
            // This example returns an adapter for every type.
            return new TimestampAdapter<>(type);
        }
    });

    Gson gson = builder.create();
    Foobar baz = new Foobar("baz", 1);
    String json = gson.toJson(baz);
    System.out.println(json);
    System.out.println(gson.fromJson(json, Foobar.class));
}

И адаптер...

class TimestampAdapter<T> extends TypeAdapter<T> {
    private TypeToken<T> type;
    private Gson gson = new GsonBuilder().create();

    public TimestampAdapter(TypeToken<T> type) {
        this.type = type;
    }

    @Override
    public void write(JsonWriter out, T value) throws IOException {
        JsonObject object = appendIsoTimestamps(value, (JsonObject) gson.toJsonTree(value));
        TypeAdapters.JSON_ELEMENT.write(out, object);
    }

    private JsonObject appendIsoTimestamps(T src, JsonObject tree) {
        try {
            PropertyDescriptor[] descriptors = Introspector.getBeanInfo(src.getClass()).getPropertyDescriptors();
            for (PropertyDescriptor descriptor : descriptors) {
                if (descriptor.getPropertyType().equals(Timestamp.class)) {
                    Timestamp ts = (Timestamp) descriptor.getReadMethod().invoke(src);
                    tree.addProperty("iso_" + descriptor.getName(), ts.toInstant().toString());
                }
            }
            return tree;
        } catch (IllegalAccessException | InvocationTargetException | IntrospectionException e) {
            throw new JsonIOException(e);
        }
    }

    @Override
    public T read(JsonReader in) {
        return gson.fromJson(in, type.getType());
    }
}

Ответ 2

Простое, короткое и управляемое DTO решение состоит в том, чтобы создать второй геттер/сеттер с другим именем для того же поля.

public class SerializationTest {

    private String foo;

    public String getFoo() { return foo; }

    // getter for json serialization
    public String getBar() { return foo; }
}

Вам может потребоваться изменить настройки сериализации для этого, например:

objectMapper.setVisibility(PropertyAccessor.GETTER, JsonAutoDetect.Visibility.PUBLIC_ONLY);
objectMapper.setVisibility(PropertyAccessor.IS_GETTER, JsonAutoDetect.Visibility.PUBLIC_ONLY);

Также обратите внимание, что этот подход имеет потенциальные ловушки, например, при десериализации - два сеттера могут устанавливать одну и ту же переменную в другое значение.