Gson Custom Enum TypeAdapterFactory: How to support Annotation SerializedName
In previous blog, I suggest to use LowercaseEnumTypeAdapterFactory on Gson.Builder to convert enums to lowercase string, despite the fact that they’re defined in CONSTANT_CASE in the corresponding Java model, to resolve NoSuchFieldException after proguard.
However, when there are some server APIs that response uppercase string as value, and then we want to convert these uppercase strings into custom enum type directly, the previous TypeAdapter doesn’t work.
For example, the json response might be
phoneNumbers: [
{
type: "HOME",
number: 2023994
},
{
type: "MOBILE",
limit: 3929103430
}
]
And our data model definition looks as following
public class PhoneNumbers {
public List<PhoneNumber> phoneNumbers;
public static class PhoneNumber {
public Type type;
public String number;
}
public enum Type {
HOME,
MOBILE,
OFFICE
}
}
We want to convert “HOME”, “MOBILE” and “OFFICE” into enum Type directly via Gson, however, I already used LowercaseEnumTypeAdapterFactory so that Gson only serialize and deserialize enum type to “home”, “mobile” and “office”.
To add Annotation @SerializedName
for each Type constant might be good solution. (Actually, it will occur crash when using LowercaseEnumTypeAdapterFactory with proguard)
public enum Type {
@SerializedName("HOME")
HOME,
@SerializedName("MOBILE")
MOBILE,
@SerializedName("OFFICE")
OFFICE
}
But it still crashed and Gson cannot recognize those SerializedName. Why?
Let’s take a look into the implementation of LowercaseEnumTypeAdapterFactory, the Gson example:
public class LowercaseEnumTypeAdapterFactory implements TypeAdapterFactory {
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
Class<T> rawType = (Class<T>) type.getRawType();
if (!rawType.isEnum()) {
return null;
}
final Map<String, T> lowercaseToConstant = new HashMap<String, T>();
for (T constant : rawType.getEnumConstants()) {
lowercaseToConstant.put(toLowercase(constant), constant);
}
return new TypeAdapter<T>() {
public void write(JsonWriter out, T value) throws IOException {
if (value == null) {
out.nullValue();
} else {
out.value(toLowercase(value));
}
}
public T read(JsonReader reader) throws IOException {
if (reader.peek() == JsonToken.NULL) {
reader.nextNull();
return null;
} else {
return lowercaseToConstant.get(reader.nextString());
}
}
};
}
private String toLowercase(Object o) {
return o.toString().toLowerCase(Locale.US);
}
}
As I highlighted in the implementation
for (T constant : rawType.getEnumConstants()) {
lowercaseToConstant.put(toLowercase(constant), constant);
}
You will see this sample just gets enum constant’s toString() and converts to lowercase. It didn’t care about whether @SerializedName
exists or not.
So how to work around this issue ……
A.) We can use an annotation, @JsonAdapter
, that indicates the custom TypeAdapter to use with this Type field.
B.) We can extend LowercaseEnumTypeAdapterFactory to respect annotation SerializedName. If SerializedName exists, it should deserialize from JSON with the provided name value. Otherwise, it still write enums in lowercase.
And here is my implementation for case B,
public class LowercaseEnumTypeAdapterFactory implements TypeAdapterFactory {
@SuppressWarnings({"rawtypes", "unchecked"})
@Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
if (gson == null || type == null) {
return null;
} Class<T> rawType = (Class<T>) type.getRawType();
if (!rawType.isEnum()) {
return null;
}
return (TypeAdapter<T>) new EnumTypeAdapter(rawType);
} private static String toLowercase(Object o) {
return o.toString().toLowerCase(Locale.US);
} // TT is also template class, because T was usage so I cannot reuse T as template
private static final class EnumTypeAdapter<TT extends Enum<TT>> extends TypeAdapter<TT> {
private final Map<String, TT> nameToConstant = new HashMap<>();
private final Map<TT, String> constantToName = new HashMap<>(); EnumTypeAdapter(Class<TT> classOfT) {
for (TT constant : classOfT.getEnumConstants()) {
String name = constant.name(); SerializedName serializedName;
try {
serializedName = classOfT.getField(name).getAnnotation(SerializedName.class);
} catch (NoSuchFieldException e) {
serializedName = null;
}
if (serializedName == null) {
name = toLowercase(constant);
} else {
name = serializedName.value();
for (String alternate : serializedName.alternate()) {
nameToConstant.put(alternate, constant);
}
} nameToConstant.put(name, constant);
constantToName.put(constant, name);
}
} @Override
public TT read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}
return nameToConstant.get(in.nextString());
} @Override
public void write(JsonWriter out, TT value) throws IOException {
out.value(value == null ? null : constantToName.get(value));
}
}
}
EnumTypeAdapter will look around all enum constants and check whether the constant has the annotation, SerializedName, or not. It gets SerializedName by getField(name).getAnnotation(SerializedName.class)
. If SerializedName exists, it would deserialize with the provided name value or alternate name.
For example, it would get instance of SerializedName(“HOME”) from enum Type.Home, and then retrive String “HOME” from serializedName.value()
(we also record the alternate).
By this way, HOME, MOBILE and OFFICE will deserialize from and serialize to string “HOME”, “MOBILE” and “OFFICE” as we wish.