Как обойти сериализацию Gson, дающую разные результаты при использовании универсальных подстановочных знаков? - PullRequest
1 голос
/ 18 марта 2019

Рассмотрим этот пример:

static class BaseBean { String baseField = "base"; }
static class ChildBean extends BaseBean { String childField = "child"; }

static class BaseBeanHolder {
    List <? extends BaseBean> beans;

    public BaseBeanHolder(List<? extends BaseBean> beans) { this.beans = beans; }
}

static class ChildBeanHolder {
    List <ChildBean> beans;

    public ChildBeanHolder(List<ChildBean> beans) { this.beans = beans; }
}

@Test
public void mcve() {
    BaseBeanHolder baseHolder = new BaseBeanHolder(singletonList(new ChildBean()));
    System.out.println(new Gson().toJson(baseHolder));

    ChildBeanHolder childHolder = new ChildBeanHolder(singletonList(new ChildBean()));
    System.out.println(new Gson().toJson(childHolder));
}

Он печатает:

{ "бобы": [{ "baseField": "база"}]}

{ "бобы": [{ "childField": "ребенок", "baseField": "база"}]}

Итак, хотя оба списка содержат дочерние объекты, только второй держатель приводит к сериализации дочерних полей в JSON.

Я видел другие вопросы, например здесь , но мне интересно, есть ли разумные обходные пути для достижения того, чего я хочу.

Другими словами: есть ли способ иметь такой один класс "holder", который принимает либо BaseBeans, либо ChildBeans (это делает <? extends BaseBean>), и что также дает правильные результаты при сериализации экземпляров с Gson в строки JSON?

(примечание: я не могу использовать адаптеры определенного типа, так как не могу контролировать, откуда поступает этот фактический экземпляр Gson и как он настроен в нашей среде)

Ответы [ 3 ]

1 голос
/ 18 марта 2019

Обычно реализации коллекций «берут» тип из объявления поля коллекции, а не из данного элемента в List / Set / и т. Д.Нам нужно написать собственный сериализатор, который для каждого элемента находит сериализатор и использует его.Простая реализация:

class TypeAwareListJsonSeserializer implements JsonSerializer<List<?>> {
    @Override
    public JsonElement serialize(List<?> src, Type typeOfSrc, JsonSerializationContext context) {
        if (src == null) {
            return JsonNull.INSTANCE;
        }
        JsonArray array = new JsonArray();
        for (Object item : src) {
            JsonElement jsonElement = context.serialize(item, item.getClass());
            array.add(jsonElement);
        }
        return array;
    }
}

И вот как мы можем использовать это:

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.gson.annotations.JsonAdapter;

import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.List;

public class GsonApp {

    public static void main(String[] args) throws Exception {
        List<BaseBean> children = Arrays.asList(new BaseBean(), new ChildBean(), new ChildBean2());
        BaseBeanHolder baseHolder = new BaseBeanHolder(children);
        Gson gson = new GsonBuilder()
                .setPrettyPrinting()
                .create();
        System.out.println(gson.toJson(baseHolder));
    }
}

class BaseBean {
    String baseField = "base";
}

class ChildBean extends BaseBean {
    String childField = "child";
}

class ChildBean2 extends BaseBean {
    int bean2Int = 356;
}

class BaseBeanHolder {

    @JsonAdapter(TypeAwareListJsonSeserializer.class)
    private List<? extends BaseBean> beans;

    // getters, setters, toString
}

Над отпечатками кода:

{
  "beans": [
    {
      "baseField": "base"
    },
    {
      "childField": "child",
      "baseField": "base"
    },
    {
      "bean2Int": 356,
      "baseField": "base"
    }
  ]
}

РЕДАКТИРОВАТЬ

Во время сериализации мы теряем информацию о типе, которая потребуется в процессе десериализации.Я разработал информацию простого типа, которая будет храниться во время сериализации и использоваться при десериализации.Это может выглядеть следующим образом:

class TypeAwareListJsonAdapter implements JsonSerializer<List<?>>, JsonDeserializer<List<?>> {

    private final String typeProperty = "@type";

    @Override
    public JsonElement serialize(List<?> src, Type typeOfSrc, JsonSerializationContext context) {
        if (src == null) {
            return JsonNull.INSTANCE;
        }
        JsonArray array = new JsonArray();
        for (Object item : src) {
            JsonObject jsonElement = (JsonObject) context.serialize(item, item.getClass());
            jsonElement.addProperty(typeProperty, item.getClass().getSimpleName());

            array.add(jsonElement);
        }
        return array;
    }

    @Override
    public List<?> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
        final Type elementType = $Gson$Types.getCollectionElementType(typeOfT, List.class);

        if (json instanceof JsonArray) {
            final JsonArray array = (JsonArray) json;
            final int size = array.size();
            if (size == 0) {
                return Collections.emptyList();
            }

            final List<?> suites = new ArrayList<>(size);
            for (int i = 0; i < size; i++) {
                JsonObject jsonElement = (JsonObject) array.get(i);
                String simpleName = jsonElement.get(typeProperty).getAsString();
                suites.add(context.deserialize(jsonElement, getClass(simpleName, elementType)));
            }

            return suites;
        }

        return Collections.emptyList();
    }

    private Type getClass(String simpleName, Type defaultType) {
        try {
            // you can use mapping or something else...
            return Class.forName("com.model." + simpleName);
        } catch (ClassNotFoundException e) {
            return defaultType;
        }
    }
}

Самая большая проблема заключается в том, как сопоставить классы значениям JSON.Мы можем использовать простое имя класса или предоставить Map<String, Class> и использовать его.Теперь мы можем использовать его, как указано выше.Пример приложения печатается сейчас:

{
  "beans": [
    {
      "baseField": "base",
      "@type": "BaseBean"
    },
    {
      "childField": "child",
      "baseField": "base",
      "@type": "ChildBean"
    },
    {
      "bean2Int": 356,
      "baseField": "base",
      "@type": "ChildBean2"
    }
  ]
}
BaseBean{baseField='base'}
ChildBean{baseField='base', childField='child'}
ChildBean2{baseField='base', bean2Int=356}
1 голос
/ 18 марта 2019

Дополнительные сведения: я только что подумал, что по крайней мере сериализация отлично работает с массивами , поэтому простым обходным решением было переделать держатель:

static class BaseBeanHolder {
    BaseBean[] beans;
    public BaseBeanHolder(BaseBean... beans) { this.beans = beans; }
}
1 голос
/ 18 марта 2019

Gson построен с учетом "Я собираюсь быть использован для сериализации" и"Я буду использоваться для десериализации".

Нет никакого способа определить изraw JSON - точный тип времени выполнения потомка BaseBean.

Вы можете использовать RuntimeTypeAdapterFactory, как описано здесь - к сожалению, он не публикуется с базовым модулем Gson и не являетсяв Maven Central, как описано здесь .Это опубликует достаточно информации с JSON, что позволит Gson десериализовать его.

...