如何使用 Gson 将 JSON 映射转换为自定义 Java 列表? app/build.gradle:JsonConverterTest.java

问题描述

给定一个这样的 JSON 映射:

{
    "category1": [],"category2": ["item1"],"category3": ["item1","item2"]
}

我想把它转换成一个 Java ArrayList of Categories(不是地图),像这样:

class Category {
  final String categoryName;
  final ArrayList<String> items;

  public Category(String categoryName,ArrayList<String> items) {
    this.categoryName = categoryName;
    this.items = items;
  }
}

ArrayList<Category> data = new ArrayList<>();

所以我希望 data 看起来像这样:

for (Category c: data) {
  System.out.println(c.categoryName + "," + c.items);
}
/**
Expected data =
category1,empty list [] or null
category2,[item1]
category2,[item1,item2]
**/

我尝试过使用 Gson 库:

app/build.gradle:

dependencies {
   ...
   implementation 'com.google.code.gson:gson:2.8.6'
}

JsonConverterTest.java

import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonParser;
import com.google.gson.internal.LinkedTreeMap;
import com.google.gson.reflect.Typetoken;

import org.junit.Test;

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

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertthat;

public class JsonConverterTest {

  @Test
  public void gsonMapToListtest() {
    String json = "{\"category1\": [],\"category2\": [\"item1\"],\"category3\": [\"item1\",\"item2\"]}";

    class Category {
      final String categoryName;
      final ArrayList<String> items;

      public Category(String categoryName,ArrayList<String> items) {
        this.categoryName = categoryName;
        this.items = items;
      }
    }

    ArrayList<Category> expectedData = new ArrayList<>();
    expectedData.add(new Category("category1",new ArrayList<String>()));
    expectedData.add(new Category("category2",new ArrayList<>(Arrays.asList("item1"))));
    expectedData.add(new Category("category2",new ArrayList<>(Arrays.asList("item1","item2"))));

    System.out.println("Expected Data:");
    for (Category c: expectedData) {
      System.out.println(c.categoryName + "," + c.items);
    }

    // This works,but it returns a Map instead of a List
    LinkedTreeMap<String,ArrayList<String>> dataMap = new Gson().fromJson(json,LinkedTreeMap.class);
    System.out.println("\n data as Map = " + dataMap);

    // This causes "IllegalStateException: Expected BEGIN_ARRAY but was BEGIN_OBJECT at line 1 column 2 path $"
    Type listofCategoriesType = new Typetoken<ArrayList<Category>>() {}.getType();
    ArrayList<Category> data = new Gson().fromJson(json,listofCategoriesType); // IllegalStateException here
    assertthat(data,is(expectedData));

    // This causes "java.lang.IllegalStateException: Not a JSON Array: {...}"
    JsonArray jsonArray = JsonParser.parseString(json).getAsJsonArray(); // IllegalStateException here
    data = new Gson().fromJson(jsonArray,listofCategoriesType);
    assertthat(data,is(expectedData));
  }
}

使用指南 https://www.baeldung.com/gson-listhttps://futurestud.io/tutorials/gson-mapping-of-maps ,我只能将 JSON 映射转换为 Java 映射。但是,如果我尝试将 JSON 映射转换为 Java 列表,则会收到 IllegalStateException:

Type listofCategoriesType = new Typetoken<ArrayList<Category>>() {}.getType();
ArrayList<Category> data = new Gson().fromJson(json,listofCategoriesType); // IllegalStateException

JsonArray jsonArray = JsonParser.parseString(json).getAsJsonArray(); // IllegalStateException
ArrayList<Category> data2 = new Gson().fromJson(jsonArray,listofCategoriesType);

那么根据上面的单元测试 gsonMapToListtest(),Gson 中将 Json Map 转换为 Java 列表的正确方法是什么?

解决方法

最简单的方法是简单地解析为Map,然后将Map转换为List<Category>

LinkedTreeMap<String,ArrayList<String>> dataMap = new Gson().fromJson(json,new TypeToken<LinkedTreeMap<String,ArrayList<String>>>() {}.getType());

ArrayList<Category> dataList = new ArrayList<>();
for (Entry<String,ArrayList<String>> entry : dataMap.entrySet())
    dataList.add(new Category(entry.getKey(),entry.getValue()));

另一种方法是编写您自己的 TypeAdapter,或使用其他 JSON 库。

,

这不是最简单的方法,但 Gson 允许实现这样的类型适配器,因此您可以在没有中间集合的情况下反序列化结果对象中的 JSON。

public final class CombinerTypeAdapterFactory<V,C>
        implements TypeAdapterFactory {

    // used to create a collection to populate
    private final TypeToken<? extends Collection<? super C>> collectionTypeToken;
    // represents a type of each map entry value
    private final TypeToken<V> valueTypeToken;
    // a strategy to map an entry to an element
    private final BiFunction<? super String,? super V,? extends C> combine;

    private CombinerTypeAdapterFactory(final TypeToken<? extends Collection<? super C>> collectionTypeToken,final TypeToken<V> valueTypeToken,final BiFunction<? super String,? extends C> combine) {
        this.collectionTypeToken = collectionTypeToken;
        this.valueTypeToken = valueTypeToken;
        this.combine = combine;
    }

    public static <V,C> TypeAdapterFactory create(final TypeToken<List<C>> collectionTypeToken,? extends C> combine) {
        return new CombinerTypeAdapterFactory<>(collectionTypeToken,valueTypeToken,combine);
    }

    @Override
    @Nullable
    public <T> TypeAdapter<T> create(final Gson gson,final TypeToken<T> typeToken) {
        // check if supported
        if ( !typeToken.equals(collectionTypeToken) ) {
            return null;
        }
        // grab the value type adapter
        final TypeAdapter<V> valueTypeAdapter = gson.getAdapter(valueTypeToken);
        // and the type adapter the collection to be populated
        final TypeAdapter<? extends Collection<? super C>> collectionTypeAdapter = gson.getDelegateAdapter(this,collectionTypeToken);
        return new TypeAdapter<T>() {
            @Override
            public void write(final JsonWriter out,final T value) {
                throw new UnsupportedOperationException(); // TODO
            }

            @Override
            public T read(final JsonReader in)
                    throws IOException {
                // assuming all of those are always { e1,e2,e3,... }
                in.beginObject();
                // a hack to create an empty collection (it's modifiable right?)
                final Collection<? super C> collection = collectionTypeAdapter.fromJson("[]");
                // for each entry in the map that's being read
                while ( in.hasNext() ) {
                    // get is name
                    final String name = in.nextName();
                    // and its value
                    final V value = valueTypeAdapter.read(in);
                    // combine
                    final C combined = combine.apply(name,value);
                    // and add to the list
                    collection.add(combined);
                }
                in.endObject();
                // we know better than javac
                @SuppressWarnings("unchecked")
                final T result = (T) collection;
                return result;
            }
        }
                .nullSafe();
    }

}

下面的测试是绿色的:

public final class CombinerTypeAdapterFactoryTest {

    @AllArgsConstructor
    @EqualsAndHashCode
    @ToString
    private static final class Category {

        @Nullable
        final String categoryName;

        @Nullable
        final List<String> items;

    }

    private static final Gson gson = new GsonBuilder()
            .disableInnerClassSerialization()
            .disableHtmlEscaping()
            .registerTypeAdapterFactory(CombinerTypeAdapterFactory.create(new TypeToken<List<Category>>() {},new TypeToken<List<String>>() {},Category::new))
            .create();

    @Test
    public void test()
            throws IOException {
        try ( final JsonReader jsonReader = new JsonReader(new InputStreamReader(CombinerTypeAdapterFactoryTest.class.getResourceAsStream("input.json"))) ) {
            final List<Category> actualCategories = gson.fromJson(jsonReader,new TypeToken<List<Category>>() {}.getType());
            Assertions.assertIterableEquals(
                    ImmutableList.of(
                            new Category("category1",ImmutableList.of()),new Category("category2",ImmutableList.of("item1")),new Category("category3",ImmutableList.of("item1","item2"))
                    ),actualCategories
            );
        }
    }

}