javaserializationgsonentryset

Java - Serializing Iterable<Map.Entry<>> with Gson


I trying to serialize an "Iterable" from type Map.Entry with google GSON library - and i got an empty output.

here an example of the code:

static private Iterable<Map.Entry<String, Integer>> getIterable(){
    HashMap<String, Integer> map = new HashMap<>();
    map.put("1", 1);
    map.put("2", 2);

    Iterable<Map.Entry<String, Integer>> iterable = map.entrySet();
    return  iterable;

}

public static void main(String[] args) {

    Iterable<Map.Entry<String, Integer>> myIterable = getIterable();
    GsonBuilder builder = new GsonBuilder();
    Gson gson = builder.enableComplexMapKeySerialization().create();
    String  a= gson.toJson(myIterable);
    System.out.println(a);

}

and this is the output :

[{},{}]

any idea what i am doing wrong?

thanks :)

java version: 1.8
gson version: 2.6.2


Solution

  • A cleaner approach might be

    final Iterable<Entry<String, Integer>> iterable = getIterable();
    final Gson gson = new Gson();
    final JsonArray jsonArray = new JsonArray();
    
    for (final Entry<String, Integer> entry : iterable) {
        final JsonElement jsonElement = gson.toJsonTree(entry);
        jsonElement.getAsJsonObject().remove("hash");
        jsonArray.add(jsonElement);
    }
    

    Or a Stream version, which I love

    StreamSupport.stream(iterable.spliterator(), false)
                 .map(gson::toJsonTree)
                 .map(JsonElement::getAsJsonObject)
                 .peek(obj -> obj.remove("hash"))
                 .collect(of(
                         JsonArray::new,
                         (array, obj) -> array.add(obj),
                         (output, toMerge) -> {
                             output.addAll(toMerge);
                             return output;
                         }
                 ));
    

    output: [{"key":"1","value":1},{"key":"2","value":2}]


    TL;DR: you need a custom TypeAdapterFactory and a custom TypeAdapter.

    See this method on TypeAdapters

    public static <TT> TypeAdapterFactory newFactory(
        final TypeToken<TT> type, final TypeAdapter<TT> typeAdapter) {
      return new TypeAdapterFactory() {
        @SuppressWarnings("unchecked") // we use a runtime check to make sure the 'T's equal
        @Override public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
          return typeToken.equals(type) ? (TypeAdapter<T>) typeAdapter : null;
        }
      };
    }
    

    enter image description here

    Without a custom TypeAdapterFactory

    typeToken.equals(type)
    

    returns false, and even a custom TypeAdapter<Entry> isn't used.


    The problem lies here, at ReflectiveTypeAdapterFactory#write

    @Override public void write(JsonWriter out, T value) throws IOException {
      if (value == null) {
        out.nullValue();
        return;
      }
    
      out.beginObject();
      try {
        for (BoundField boundField : boundFields.values()) {
          if (boundField.writeField(value)) {
            out.name(boundField.name);
            boundField.write(out, value);
          }
        }
      } catch (IllegalAccessException e) {
        throw new AssertionError(e);
      }
      out.endObject();
    }
    

    and at ReflectiveTypeAdapterFactory#getBoundFields

    private Map<String, BoundField> getBoundFields(Gson context, TypeToken<?> type, Class<?> raw) {
      Map<String, BoundField> result = new LinkedHashMap<String, BoundField>();
    
      if (raw.isInterface()) {
        return result;
      }
    

    Gson is recognizing the input Entry (Class<?> raw parameter) as

    interface Map.Entry<K, V> { ... }
    

    Therefore

    if (raw.isInterface())
    

    yield true, and an empty boundFields LinkedHashMap is returned.
    Thus, here

    for (BoundField boundField : boundFields.values()) { ... }
    

    the loop isn't executed, and no values are extracted and written with

    boundField.write(...)