Gson int type is converted to double problem solving (perfect solution)

1. Recurrence of the problem

1.1. BaseResponse class

class BaseResponse<T>(
    val code: Int = -1,
    val message: String? = null
) {
    var data: T? = null

    fun isSuccess(): Boolean {
        return code == 0
    }
}

1.2. Without any processing, directly use Gson.fromJson to parse

val json = "{\"code\":200,\"message\":\"成功\",\"data\":{\"ints\":200,\"doubles\":200.98,\"floats\":29.0986,\"longs\":29323627832875342,\"string\":\"字符串\"}}"
val baseResponse = Gson().fromJson(json, BaseResponse::class.java)
println(JsonUtils.toJson(baseResponse))

1.3. Analysis results

I/System.out: {"code":200,"data":{"ints":200.0,"doubles":200.98,"floats":29.0986,"longs":2.9323627832875344E16,"string":"字符串"},"message":"成功"}

1.4. Problem

"ints" of type Int are converted to type double, and "longs" of type Long are also converted.

2. Solution

2.1. Old (the same copying solutions on the Internet are actually of no use )

The common practice on the Internet is to create a new subclass of the TypeAdapter interface and convert it manually. MyDataTypeAdapter 

package com.xxx.baseapp.net;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.ToNumberPolicy;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.internal.LinkedTreeMap;
import com.google.gson.internal.bind.ObjectTypeAdapter;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;

import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;


public final class MyDataTypeAdapter 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 MapTypeAdapter(gson);
            }
            return null;
        }
    };

    private final Gson gson;

    private MyDataTypeAdapter(Gson gson) {
        this.gson = gson;
    }

    @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();
            case NUMBER:
                String s = in.nextString();
                if (s.contains(".")) {
                    return Double.valueOf(s);
                } else {
                    try {
                        return Integer.valueOf(s);
                    } catch (Exception e) {
                        return Long.valueOf(s);
                    }
                }
            case BOOLEAN:
                return in.nextBoolean();
            case NULL:
                in.nextNull();
                return null;
            default:
                throw new IllegalStateException();
        }
    }

    @Override
    public void write(JsonWriter out, Object value) throws IOException {
        if (value == null) {
            out.nullValue();
            return;
        }
        //noinspection unchecked
        TypeAdapter<Object> typeAdapter = (TypeAdapter<Object>) gson.getAdapter(value.getClass());
        if (typeAdapter instanceof ObjectTypeAdapter) {
            out.beginObject();
            out.endObject();
            return;
        }
        typeAdapter.write(out, value);
    }

    /**
     * 使用自定义工厂方法取代 Gson 实例中的工厂方法
     */
    public Gson getGson() {
        Gson gson = new GsonBuilder().create();
        try {
            Field factories = Gson.class.getDeclaredField("factories");
            factories.setAccessible(true);
            Object o = factories.get(gson);
            Class<?>[] declaredClasses = Collections.class.getDeclaredClasses();
            for (Class c : declaredClasses) {
                if ("java.util.Collections$UnmodifiableList".equals(c.getName())) {
                    Field listField = c.getDeclaredField("list");
                    listField.setAccessible(true);
                    List<TypeAdapterFactory> list = (List<TypeAdapterFactory>) listField.get(o);
                    int i = list.indexOf(ObjectTypeAdapter.getFactory(ToNumberPolicy.DOUBLE));
                    list.set(i, MyDataTypeAdapter.FACTORY);
                    break;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return gson;
    }

} 

2.2. Modify the plan (almost solved after reading the source code)

2.2.1. Analysis

By analyzing the source code of GsonBuilder's construction method:

   /**
   * Creates a GsonBuilder instance that can be used to build Gson with various configuration
   * settings. GsonBuilder follows the builder pattern, and it is typically used by first
   * invoking various configuration methods to set desired options, and finally calling
   * {@link #create()}.
   */
  public GsonBuilder() {
  }

  /**
   * Constructs a GsonBuilder instance from a Gson instance. The newly constructed GsonBuilder
   * has the same configuration as the previously built Gson instance.
   *
   * @param gson the gson instance whose configuration should by applied to a new GsonBuilder.
   */
  GsonBuilder(Gson gson) {
    ......
    this.objectToNumberStrategy = gson.objectToNumberStrategy;
    this.numberToNumberStrategy = gson.numberToNumberStrategy;
    this.reflectionFilters.addAll(gson.reflectionFilters);
  }


  /**
   * Configures Gson to apply a specific number strategy during deserialization of {@link Object}.
   *
   * @param objectToNumberStrategy the actual object-to-number strategy
   * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
   * @see ToNumberPolicy#DOUBLE The default object-to-number strategy
   * @since 2.8.9
   */
  public GsonBuilder setObjectToNumberStrategy(ToNumberStrategy objectToNumberStrategy) {
    this.objectToNumberStrategy = Objects.requireNonNull(objectToNumberStrategy);
    return this;
  }

It can be found that the processing strategy for converting Object and Number classes into numerical types is defaulted during construction. It can be seen that Object conversion defaults to the dobule type of ToNumberPolicy.DOUBLE, so not only int will be converted into double and Long types. Numerical types will be converted to dobule types by default.

2.2.2. Strategy category

/**
 * An enumeration that defines two standard number reading strategies and a couple of
 * strategies to overcome some historical Gson limitations while deserializing numbers as
 * {@link Object} and {@link Number}.
 *
 * @see ToNumberStrategy
 * @since 2.8.9
 */
public enum ToNumberPolicy implements ToNumberStrategy {

  /**
   * Using this policy will ensure that numbers will be read as {@link Double} values.
   * This is the default strategy used during deserialization of numbers as {@link Object}.
   */
  DOUBLE {
    @Override public Double readNumber(JsonReader in) throws IOException {
      return in.nextDouble();
    }
  },

  /**
   * Using this policy will ensure that numbers will be read as a lazily parsed number backed
   * by a string. This is the default strategy used during deserialization of numbers as
   * {@link Number}.
   */
  LAZILY_PARSED_NUMBER {
    @Override public Number readNumber(JsonReader in) throws IOException {
      return new LazilyParsedNumber(in.nextString());
    }
  },

  /**
   * Using this policy will ensure that numbers will be read as {@link Long} or {@link Double}
   * values depending on how JSON numbers are represented: {@code Long} if the JSON number can
   * be parsed as a {@code Long} value, or otherwise {@code Double} if it can be parsed as a
   * {@code Double} value. If the parsed double-precision number results in a positive or negative
   * infinity ({@link Double#isInfinite()}) or a NaN ({@link Double#isNaN()}) value and the
   * {@code JsonReader} is not {@link JsonReader#isLenient() lenient}, a {@link MalformedJsonException}
   * is thrown.
   */
  LONG_OR_DOUBLE {
    @Override public Number readNumber(JsonReader in) throws IOException, JsonParseException {
      String value = in.nextString();
      try {
        return Long.parseLong(value);
      } catch (NumberFormatException longE) {
        try {
          Double d = Double.valueOf(value);
          if ((d.isInfinite() || d.isNaN()) && !in.isLenient()) {
            throw new MalformedJsonException("JSON forbids NaN and infinities: " + d + "; at path " + in.getPreviousPath());
          }
          return d;
        } catch (NumberFormatException doubleE) {
          throw new JsonParseException("Cannot parse " + value + "; at path " + in.getPreviousPath(), doubleE);
        }
      }
    }
  },

  /**
   * Using this policy will ensure that numbers will be read as numbers of arbitrary length
   * using {@link BigDecimal}.
   */
  BIG_DECIMAL {
    @Override public BigDecimal readNumber(JsonReader in) throws IOException {
      String value = in.nextString();
      try {
        return new BigDecimal(value);
      } catch (NumberFormatException e) {
        throw new JsonParseException("Cannot parse " + value + "; at path " + in.getPreviousPath(), e);
      }
    }
  }

}

Through the above policy class source code, we found that: LAZILY_PARSED_NUMBER  is an enumeration that processes numerical types. Looking at the LazilyParsedNumber source code, we found that detailed numerical type processing is indeed divided:

/**
 * This class holds a number value that is lazily converted to a specific number type
 *
 * @author Inderjeet Singh
 */
@SuppressWarnings("serial") // ignore warning about missing serialVersionUID
public final class LazilyParsedNumber extends Number {
  private final String value;

  /** @param value must not be null */
  public LazilyParsedNumber(String value) {
    this.value = value;
  }

  @Override
  public int intValue() {
    try {
      return Integer.parseInt(value);
    } catch (NumberFormatException e) {
      try {
        return (int) Long.parseLong(value);
      } catch (NumberFormatException nfe) {
        return new BigDecimal(value).intValue();
      }
    }
  }

  @Override
  public long longValue() {
    try {
      return Long.parseLong(value);
    } catch (NumberFormatException e) {
      return new BigDecimal(value).longValue();
    }
  }

  @Override
  public float floatValue() {
    return Float.parseFloat(value);
  }

  @Override
  public double doubleValue() {
    return Double.parseDouble(value);
  }

  @Override
  public String toString() {
    return value;
  }

  /**
   * If somebody is unlucky enough to have to serialize one of these, serialize
   * it as a BigDecimal so that they won't need Gson on the other side to
   * deserialize it.
   */
  private Object writeReplace() throws ObjectStreamException {
    return new BigDecimal(value);
  }

  private void readObject(ObjectInputStream in) throws IOException {
    // Don't permit directly deserializing this class; writeReplace() should have written a replacement
    throw new InvalidObjectException("Deserialization is unsupported");
  }

  @Override
  public int hashCode() {
    return value.hashCode();
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj) {
      return true;
    }
    if (obj instanceof LazilyParsedNumber) {
      LazilyParsedNumber other = (LazilyParsedNumber) obj;
      return value == other.value || value.equals(other.value);
    }
    return false;
  }
}

 2.2.2. Final solution

Now that we know that the default  ToNumberPolicy.DOUBLE is set during construction , it is simple to modify it. We can call the policy method to set the conversion:

fun getGson(): Gson {
    val gsonBuilder = GsonBuilder()
    gsonBuilder.setObjectToNumberStrategy(ToNumberPolicy.LAZILY_PARSED_NUMBER)
    return gsonBuilder.create()
}

Call getGson analysis after modifying the strategy plan 

val json = "{\"code\":200,\"message\":\"成功\",\"data\":{\"ints\":200,\"doubles\":200.98,\"floats\":29.0986,\"longs\":29323627832875342,\"string\":\"字符串\"}}"
val baseResponse = getGson().fromJson(json, BaseResponse::class.java)
println(JsonUtils.toJson(baseResponse))

Test Results:

I/System.out: {"code":200,"data":{"ints":200,"doubles":200.98,"floats":29.0986,"longs":29323627832875342,"string":"字符串"},"message":"成功"}

This is the perfect solution! ! ! !

The Internet is full of copy and paste saying that this can be solved and that can be solved, but the results are inconsistent. This problem bothered me for 2 days, and finally it was solved perfectly

Guess you like

Origin blog.csdn.net/TomLeisen/article/details/129848521