Have a problem
Recently, we received user feedback that some interface request data failed, and the debugging interface found that it was caused by the uncertain type of background return. For example, this is a normal JSON we need:
{
"id": 1,"number": 100000000,"user": {"name":"aoteman"."userId": 110}}Copy the code
But the background sometimes returns json like this:
{
"id":""."number":""."user":""
}
Copy the code
As you can see, the id of int, number of long, and user of Object are all empty strings. If the result is null, it may return an empty string. You can guess that PHP is a weakly typed language, and there is no type verification for the field. This was supposed to be the background pot, but the communication between departments was difficult, so we had to solve it first.
The solution
Gson provides a registerTypeAdapter method in GsonBuilder that lets you customize the serialization and deserialization of a particular model. There are two ways to serialize and deserialize:
- The first type is derived from JsonSerializer and JsonDeserializer interfaces. The code for JsonDeserializer is as follows:
public class Deserializer implements JsonDeserializer<Test> {
@Override
public Test deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context)
throws JsonParseException {
final JsonObject jsonObject = json.getAsJsonObject();
final JsonElement jsonId = jsonObject.get("id");
final JsonElement jsonNumber = jsonObject.get("number");
final Test test= new Test(); try { test.setId(jsonId.getAsInt()); }catch (Exception e){// if id is not an int, catch an Exception and setId to 0 test.setid (0); } try { test.setNumber(jsonNumber.getAsLong()); }catch (Exception e){ test.setNumber(0); }return test; }}Copy the code
JsonElement has four subclasses: JsonObject (object structure), JsonArray (array structure), JsonPrimitive (primitive type) and JsonNull. JsonObject actually maintains a HashMap inside, jsonObject.get(“id”); It’s just looking up fields. This approach is costly in terms of performance because it requires converting the stream data into JsonElement structural objects first, which results in higher memory consumption and runtime.
- The second type inherits from TypeAdapter with the following code:
new GsonBuilder().registerTypeAdapter(Test.class,
new TypeAdapter<Test>() {
public Test read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}
in.beginObject();
Test test = new Test();
while (in.hasNext()) {
switch (in.nextName()) {
case "id":
try {
test.setId(in.nextInt());
} catch (Exception e) {
in.nextString();
test.setId(0);
}
break;
case "number":
try {
test.setNumber(in.nextLong());
} catch (Exception e) {
in.nextString();
test.setNumber(0);
}
break; }}return test;
}
public void write(JsonWriter out, Test src) throws IOException {
if (src == null) {
out.nullValue();
return; }}})Copy the code
This approach is more efficient than JsonElement because it parses data directly by streaming. Without JsonElement, the streaming API is more efficient than the first tree parsing API.
The solution to the problem could be as follows:
Private static TypeAdapter<Number> INTEGER = new TypeAdapter<Number>() {@override public Numberread(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}
try {
returnin.nextInt(); } catch (NumberFormatException e) {nextString() is called because nextInt failed to complete the displacement. in.nextString();return0; } } @Override public void write(JsonWriter out, Number value) throws IOException { out.value(value); }}; private static TypeAdapter<Number> LONG = new TypeAdapter<Number>() { @Override public Numberread(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
returnnull; } // same as try {return in.nextLong();
} catch (Exception e) {
in.nextString();
}
return0; } @Override public void write(JsonWriter out, Number value) throws IOException { out.value(value); }}; Gson gson = new GsonBuilder() .registerTypeAdapterFactory(TypeAdapters.newFactory(int.class, Integer.class, INTEGER)) .registerTypeAdapterFactory(TypeAdapters.newFactory(long.class, Long.class, LONG)) .create();Copy the code
This code solves the problem of returning empty strings for int and long. It is not possible to register every type. Let’s look at Gson’s constructor and find the following code:
factories.add(new ReflectiveTypeAdapterFactory(
constructorConstructor, fieldNamingPolicy, excluder));
Copy the code
See ReflectiveTypeAdapterFactory content found that this is for all user-defined type of the parser. Modify the problem mainly locate in ReflectiveTypeAdapterFactory Adapter in the class read method. We can solve the problem simply by modifying the read method:
@Override
public T read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
returnnull; } T instance = constructor.construct(); BeginObject () {beginObject ()""Try {in.beginObject(); } catch (Exception e) { in.nextString();return null;
}
try {
int count = 0;
while (in.hasNext()) {
count++;
String name = in.nextName();
BoundField field = boundFields.get(name);
if(field == null || ! field.deserialized) { in.skipValue(); }else {
field.read(in, instance); }}if (count == 0) return null;
} catch (IllegalStateException e) {
throw new JsonSyntaxException(e);
} catch (IllegalAccessException e) {
throw new AssertionError(e);
}
in.endObject();
return instance;
}
Copy the code
ReflectiveTypeAdapterFactory is a final class cannot be inherited, and construction method are introduced from Gson several important parameters, so we can only make a copy of ReflectiveTypeAdapterFactory out modification, And use reflection to modify Gson, replace the modified class with the original, the code is as follows:
public static Gson buildGson() {
Gson gson = new GsonBuilder().registerTypeAdapterFactory(TypeAdapters.newFactory(int.class, Integer.class, INTEGER))
.registerTypeAdapterFactory(TypeAdapters.newFactory(long.class, Long.class, LONG))
.create();
try {
Field field = gson.getClass().getDeclaredField("constructorConstructor");
field.setAccessible(true);
ConstructorConstructor constructorConstructor = (ConstructorConstructor) field.get(gson);
Field factories = gson.getClass().getDeclaredField("factories");
factories.setAccessible(true);
List<TypeAdapterFactory> data = (List<TypeAdapterFactory>) factories.get(gson);
List<TypeAdapterFactory> newData = new ArrayList<>(data);
newData.remove(data.size() - 1);
newData.add(new MyReflectiveTypeAdapterFactory(constructorConstructor, FieldNamingPolicy.IDENTITY, Excluder.DEFAULT));
newData = Collections.unmodifiableList(newData);
factories.set(gson, newData);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return gson;
}
Copy the code
The problem is solved here, because the project rarely uses fields of type float, byte, etc., so there is no adaptation, and this can be done if necessary. The sample code is hosted on Github.