Introduction: In daily development, there are many opportunities to deal with JSON, the general object json conversion will not have any problems, but json to object may have problems, today let’s talk about json to map caused by int type conversion to double problem

Problem reproduction

  • We solved the problem of long being converted to scientific notation, so we took the old common method, a generic utility class
public class MyType<T> {
    public T gsonToMap(String strJson) {
        return new Gson().fromJson(strJson, new TypeToken<T>() {
        }.getType());
    }
}

String json = "{\"identifier\":\"18111111111\",\"opType\":1,\"platform\":0}";
Map<String, Object> map = new MyType<Map<String, Object>>().gsonToMap(json);
Copy the code
  • Just pass the requirement type object directly to the generic.
  • Instead, int was successfully converted to double, 1->1.0 and 0->0.0, as shown in the figure above

The following operation is known to all, with the help of the network platform, so I found several solutions, carefully I found that some people comment to solve their problems, it seems to be a play.

The solution

1, need gson to parse the type, rewrite his deserialize method, is the json manually parsed into a map, no data processing

public HashMap<String,Object> gsonToMap(String strJson) {
        Gson gson = new GsonBuilder()
                .registerTypeAdapter(
                new TypeToken<HashMap<String,Object>>(){}.getType(),
                        new JsonDeserializer<HashMap<String, Object>>() {
                            @Override
                            public HashMap<String, Object> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {

                                HashMap<String, Object> hashMap = new HashMap<>();
                                JsonObject jsonObject = json.getAsJsonObject();
                                Set<Map.Entry<String, JsonElement>> entrySet = jsonObject.entrySet();
                                for (Map.Entry<String, JsonElement> entry : entrySet) {
                                    hashMap.put(entry.getKey(), entry.getValue());
                                }
                                return hashMap;
                            }
                        }).create();

        return gson.fromJson(strJson, new TypeToken<HashMap<String,Object>>() {
        }.getType());
    }
Copy the code
  • In practice, it works, but in the spirit of reuse, I replace map with generics, and then it doesn’t work. (The question is on the back burner.)

2, custom TypeAdapter to replace the default adapter Gson, custom TypeAdapter is as follows:

public class MapTypeAdapter extends TypeAdapter<Object> {

    private final TypeAdapter<Object> delegate = new Gson().getAdapter(Object.class);

    @Override
    public Object read(JsonReader in) throws IOException {
        JsonToken token = in.peek();
        switch (token) {
            case BEGIN_ARRAY:
                List<Object> list = new ArrayList<>();
                in.beginArray();
                while (in.hasNext()) {
                    list.add(read(in));
                }
                in.endArray();
                return list;

            case BEGIN_OBJECT:
                Map<String, Object> map = new LinkedTreeMap<>();
                in.beginObject();
                while (in.hasNext()) {
                    map.put(in.nextName(), read(in));
                }
                in.endObject();
                return map;
                
            case STRING:
                return in.nextString();

            caseNUMBER: /** * Rewrite the NUMBER processing logic, NUMBER values into integer and floating point type. */ double dbNum = in.nextDouble(); // If the number exceeds the maximum value of long, return a floating-point typeif (dbNum > Long.MAX_VALUE) {
                    returnString.valueOf(dbNum); } long lngNum = (long) dbNum;if (dbNum == lngNum) {
                    return String.valueOf(lngNum);
                } else {
                    return String.valueOf(dbNum);
                }

            case BOOLEAN:
                return in.nextBoolean();

            case NULL:
                in.nextNull();
                returnnull; default: throw new IllegalStateException(); } } @Override public void write(JsonWriter out, Object value) throws IOException { delegate.write(out,value); }}Copy the code
  • Then we did the same thing, sticking with generics and registering our own custom to Gson
public T gsonToMap(String strJson) {
        Gson gson = new GsonBuilder()
                .registerTypeAdapter(new TypeToken<T>(){}.getType(),new MapTypeAdapter()).create();
        return gson.fromJson(strJson, new TypeToken<T>() {
        }.getType());
    }
    
String json = "{\"identifier\":\"18111111111\",\"opType\":1,\"platform\":0}";
Map<String, Object> map = new MyType<Map<String, Object>>().gsonToMap(json);
Copy the code
  • Waiting for the results… Every error is so exciting, int will also be converted to double

  • Replace the generic type directly with the target object type, try again, and it works
public static Map<String, Object> gsonToMap(String strJson) {
        Gson gson = new GsonBuilder()
                .registerTypeAdapter(new TypeToken<Map<String,Object>>(){}.getType(),new MapTypeAdapter()).create();
        return gson.fromJson(strJson, new TypeToken<Map<String, Object>>() {
        }.getType());
    }
    
String json = "{\"identifier\":\"18111111111\",\"opType\":1,\"platform\":0}";
Map<String, Object> map = new MyType<Map<String, Object>>().gsonToMap(json);
Copy the code

The above solution did solve my problem, but it left me with doubts; With the purpose of knowing what is what and why, we resolve these doubts

Solve the doubts

  • Why not pass generics?
  • Why convert an int to a double instead of, say, a string?

1. Generics here is the point about generic erasure and the fact that generics only work at compile time, not run time

  • If you trace the source code, you’ll see that TypeAdapter is already a generic abstract class
public abstract class TypeAdapter<T>
Copy the code
  • I pass the generic again in the outer layer, and the runtime does not know the target object type I pass
  • In the outer layer, I pass the target Object type directly. Here I pass HashMap

    , but I recognize it exactly correctly
    ,object>

  • So what I’m doing here is completely generic erase, so the runtime code doesn’t even know what this is, so you’re not going to get what we want, right

2, int to double, actually this is Gson in the source code intentionally, in fact, not only int,long will also be converted to double, next we go to find evidence

  • Trace the source code, go you => process omit 1000 steps, ignore 1000000 words, we will come to this place under Gson
  • This handles adapters of type Number, and in addition
Dealing with a double: private TypeAdapter < Number > doubleAdapter (Boolean serializeSpecialFloatingPointValues) {if (serializeSpecialFloatingPointValues) {
      return TypeAdapters.DOUBLE;
    }
    return new TypeAdapter<Number>() {
      @Override public Double read(JsonReader in) throws IOException {
        if (in.peek() == JsonToken.NULL) {
          in.nextNull();
          return null;
        }
        return in.nextDouble();
      }
      @Override public void write(JsonWriter out, Number value) throws IOException {
        if (value == null) {
          out.nullValue();
          return; } double doubleValue = value.doubleValue(); checkValidFloatingPoint(doubleValue); out.value(value); }}; } processingfloat:
  private TypeAdapter<Number> floatAdapter(boolean serializeSpecialFloatingPointValues) {
    if (serializeSpecialFloatingPointValues) {
      return TypeAdapters.FLOAT;
    }
    return new TypeAdapter<Number>() {
      @Override public Float read(JsonReader in) throws IOException {
        if (in.peek() == JsonToken.NULL) {
          in.nextNull();
          return null;
        }
        return (float) in.nextDouble();
      }
      @Override public void write(JsonWriter out, Number value) throws IOException {
        if (value == null) {
          out.nullValue();
          return;
        }
        float floatValue = value.floatValue();
        checkValidFloatingPoint(floatValue); out.value(value); }}; }Copy the code
  • We’re actually looking for the type that we want to match our target object, but if we don’t find a match, we’ll call ObjectTypeAdapter, keep tracking, and it’s finally looking for the adapter that it likes.

  • We had better luck. This isfor (TypeAdapterFactory factory : factories)There are over 40 adapters, the second one we’re looking forObjectTypeAdapter
  • As soon as it sees everyone is T, you and I look the most like, that call you, and then came to the new world
public final class ObjectTypeAdapter extends TypeAdapter<Object> {
  public static final TypeAdapterFactory FACTORY = new TypeAdapterFactory() {
    @SuppressWarnings("unchecked")
    @Override public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
      if (type.getRawType() == Object.class) {
        return (TypeAdapter<T>) new ObjectTypeAdapter(gson);
      }
      returnnull; }}; private final Gson gson; ObjectTypeAdapter(Gson gson) { this.gson = gson; } @Override public Objectread(JsonReader in) throws IOException {
    JsonToken token = in.peek();
    switch (token) {
    case BEGIN_ARRAY:
      List<Object> list = new ArrayList<Object>();
      in.beginArray();
      while (in.hasNext()) {
        list.add(read(in));
      }
      in.endArray();
      return list;

    case BEGIN_OBJECT:
      Map<String, Object> map = new LinkedTreeMap<String, Object>();
      in.beginObject();
      while (in.hasNext()) {
        map.put(in.nextName(), read(in));
      }
      in.endObject();
      return map;

    case STRING:
      return in.nextString();

    case NUMBER:
      return in.nextDouble();

    case BOOLEAN:
      return in.nextBoolean();

    case NULL:
      in.nextNull();
      return null;

    default:
      throw new IllegalStateException();
    }
  }

  @SuppressWarnings("unchecked")
  @Override public void write(JsonWriter out, Object value) throws IOException {
    if (value == null) {
      out.nullValue();
      return;
    }

    TypeAdapter<Object> typeAdapter = (TypeAdapter<Object>) gson.getAdapter(value.getClass());
    if (typeAdapter instanceof ObjectTypeAdapter) {
      out.beginObject();
      out.endObject();
      return;
    }

    typeAdapter.write(out, value); }}Copy the code
  • Is it the same as the customized adapter we created before, that’s why we want to duplicate the TypeAdapter, focus on the following
case NUMBER:
      return in.nextDouble();
Copy the code
  • Any type that is Number (int, long, float, double, etc.) is forced to be converted to double, but not the other way around.

In fact, there is an even easier way, which is to use no Gson, such as FastJson, without this problem at all

Postscript: solving the problem is not fundamental, we need to find the root of the problem and prevent its occurrence from the root, which needs to be strengthened in the future. Refueling (if there is a mistake, welcome to comment)!!