discomarathon / google-gson

Automatically exported from code.google.com/p/google-gson
0 stars 0 forks source link

default parsing of Map where generic value param is object #325

Closed GoogleCodeExporter closed 9 years ago

GoogleCodeExporter commented 9 years ago
It seems we could make a better parser for Map<String,Object> which converts 
the Object to a primitive wrapper, list, or map.  Right now, we just get an 
error back.

"Type information is unavailable, and the target object is not a primitive"

For example, the following test should work, ideally:

   public void testMapStringObjectWithAllValidValuesOneDeep() {
      Map<String, Object> map = Maps.newHashMap();
      map.put("string", "string");
      map.put("number", 1);
      map.put("boolean", true);
      map.put("map", ImmutableMap.of("key", "value"));
      map.put("list", ImmutableList.of("key", "value"));
      assertEquals(json.toJson(map),
            "{\"string\":\"string\",\"map\":{\"key\":\"value\"},\"list\":[\"key\",\"value\"],\"boolean\":true,\"number\":1}");
      Map<String, Object> map2 = json.fromJson(json.toJson(map), new TypeLiteral<Map<String, Object>>() {
      }.getType());
      assertEquals(map2, map);
      assertEquals(json.toJson(map2), json.toJson(map));
   }

Original issue reported on code.google.com by adrian.f...@gmail.com on 15 May 2011 at 12:15

GoogleCodeExporter commented 9 years ago
Here's the patch for this:

in MapTypeAdapter, add this where the value is being parsed

      Object value = null;
      if (keyAndValueTypes[1] == Object.class) {
         value = ParseObjectFromElement.SINGLETON.apply(entry.getValue());
      }
      if (value == null) {
         value = context.deserialize(entry.getValue(), keyAndValueTypes[1]);
      }

ParseObjectFromElement and deps are below:

public enum ParseObjectFromElement implements Function<JsonElement, Object> {
   SINGLETON;
   public Object apply(JsonElement input) {
      Object value = null;
      if (input == null || input.isJsonNull()) {
         value = null;
      } else if (input.isJsonPrimitive()) {
         JsonPrimitive primitive = input.getAsJsonPrimitive();
         if (primitive.isNumber()) {
            value = primitive.getAsNumber();
         } else if (primitive.isBoolean()) {
            value = primitive.getAsBoolean();
         } else {
            value = primitive.getAsString();
         }
      } else if (input.isJsonArray()) {
         value = Lists.newArrayList(Iterables.transform(input.getAsJsonArray(), this));
      } else if (input.isJsonObject()) {
         value = Maps.<String,Object>newLinkedHashMap(Maps.transformValues(JsonObjectAsMap.INSTANCE.apply(input.getAsJsonObject()),
               this));
      }
      return value;
   }
}

public enum JsonObjectAsMap implements Function<JsonObject, Map<String, 
JsonElement>> {
   INSTANCE;

   private final Field members;

   JsonObjectAsMap() {
      try {
         members = JsonObject.class.getDeclaredField("members");
         members.setAccessible(true);
      } catch (NoSuchFieldException e) {
         throw new UnsupportedOperationException("cannot access gson internals", e);
      }
   }

   @SuppressWarnings("unchecked")
   @Override
   public Map<String, JsonElement> apply(JsonObject in) {
      try {
         return (Map<String, JsonElement>) members.get(in);
      } catch (IllegalArgumentException e) {
         throw new UnsupportedOperationException("cannot access gson internals", e);
      } catch (IllegalAccessException e) {
         throw new UnsupportedOperationException("cannot access gson internals", e);
      }
   }
}

Original comment by adrian.f...@gmail.com on 15 May 2011 at 12:17

GoogleCodeExporter commented 9 years ago
If you want to parse an arbitrary map, how about using JsonParser to parse the 
JSON into a JsonElement DOM. You can then navigate the DOM and deserialize 
using gson.fromJson(JsonElement, Type) method.

Original comment by inder123 on 15 May 2011 at 6:53

GoogleCodeExporter commented 9 years ago
[deleted comment]
GoogleCodeExporter commented 9 years ago
Implementation of this change would also provide better consistency with the 
behavior when simple object fields of type "Object" are deserialized to.

For example, simple deserialization of 

{"one":"won","two":2,"three":false}

into 

class ObjectThings
{
  Object one;
  Object two;
  Object three;
}

using

ObjectThings objectThings = gson.fromJson(new FileReader("input.json"), 
ObjectThings.class);

generates an instance of ObjectThings where the fields "one", "two", and 
"three" are populated with instances of String, LazilyParsedNumber, and Boolean 
respectively.

However, when deserializing this same JSON using 

Type mapStringObjectType = new TypeToken<Map<String, Object>>(){}.getType();
Map<String, Object> mapStringObject = gson.fromJson(new 
FileReader("input.json"), mapStringObjectType);

this creates a map with three entries (with keys "one", "two", "three"), where 
each entry value is a mostly useless instance of Object, containing of course 
no deserialized data.  This is an inconsistent result when compared to the 
first example I posted.  To be consistent, the types of the map entries values 
should be String, LazilyParsedNumber, and Boolean, with values "won", 2, false.

It is certainly possible for users to implement a custom deserializer for this 
scenario, however doing so somewhat defeats the entire purpose for using an API 
like Gson, and, again, the necessity to do so is inconsistent with the 
deserialization behavior of the first example I posted.

A very simple alteration to Gson to provide deserialization to Map<String, 
Object> consistent with deserialization to ObjectThings {Object one; Object 
two; Object three;} is in JsonDeserializationContext.fromJsonPrimitive(Type, 
JsonPrimitive, JsonDeserializationContext) to change 

objectNavigator.accept(new ObjectTypePair(json.getAsObject(), typeOfT, true), 
visitor);

to

objectNavigator.accept(new ObjectTypePair(null, typeOfT, true), visitor);

This change introduces no new failures to the current Gson test suite.  (The 
test suite currently contains six failures based on date indexes off by one 
position.  This is not affected by the change I just described.)

An alternative solution to this problem, so as not to affect the current API 
behavior, may be the changes described above by Adrian (I don't know, as I 
didn't yet go through them), or it may be to add a configuration to GsonBuilder 
along the following lines 

public GsonBuilder enableLazyJsonPrimitiveMapValueDeserialization() {
  registerTypeHierarchyAdapter(Map.class, PRIMITIVES_AS_MAP_VALUES_TYPE_ADAPTER);
  return this;
}

with an appropriate implementation of a new MapTypeAdapter type.

If there is any interest amongst the Gson project maintainers for this change, 
I'm glad to provide an implementation along with appropriate test cases.

Original comment by Programm...@gmail.com on 13 Jun 2011 at 5:52

GoogleCodeExporter commented 9 years ago
A recent post on StackOverflow.com helps demonstrate the value of implementing 
a reasonable solution for issue 325.  http://stackoverflow.com/questions/6455303

Solution With Gson without 325 Implemented: Fugly
Solution With Jackson: 1 Line

Original comment by Programm...@gmail.com on 24 Jun 2011 at 2:21

GoogleCodeExporter commented 9 years ago
Fixed in GSON 2.0. I also answered the stackoverflow question.

Original comment by limpbizkit on 1 Oct 2011 at 4:55

GoogleCodeExporter commented 9 years ago
looks great.  thanks

Original comment by adrian.f...@gmail.com on 1 Jan 2012 at 10:57