Home Gson如何优雅的处理Enum类
Post
Cancel

Gson如何优雅的处理Enum类

1. 遇到的问题

Gson来序列化、反序列化某个带有Enum成员变量时,会存在一点瑕疵。比如一个枚举类:

1
2
3
4
enum class Type(var type: Int) {
    FIRST(1),
    SECOND(2)
}

在序列化时,想要的结果是:

1
2
3
{
    "key": 1
}

可实际上是:

1
2
3
{
    "key": "FIRST"
}

同样,在反序列化时,json字符串传进来的是1, 无法映射到 枚举Type.FIRST


2. 解决方案

目前,网上有两种解决方案:

方案一:

通过Gson 提供的 TypeAdapterJsonSerializerJsonDeserializer类,在实例化Gson时注册进去。但是有个致命的缺点,它只能针对单个具体的Enum。如果你的项目中有20个Enum, 那就必须实现20个类,再分别注册进去。这种方式通用性不强,排除掉。

方案二:

通过Gson提供的SerializedName注解。用法如下:

1
2
3
4
5
6
enum class Type(var type: Int) {
    @SerializedName("1")
    FIRST(1),
    @SerializedName("2")
    SECOND(2)
}

加上注解之后,序列化的结果是:

1
2
3
{
    "key": "1"
}

但是这种方式也有问题。上面的例子中,type 是 Int类型,最后生成的json却是"key": "1", 而不是"key": 1

那么,要怎么处理才能解决问题并且优雅呢?


3. 最优雅的方案

本质上,是通过TypeAdapterFactory 动态的生成TypeAdapter来实现的。

具体思路是:

  1. 判断序列化、反序列化的变量是否为枚举类型。如果不是就交给Gson处理。
  2. 获取到枚举类的所有类型。
  3. 如果枚举类型有SerializedName注解,则反射拿到SerializedName.value并保存到Map。如果没有,反射拿到成员变量并保存到Map。
  4. TypeAdapterwrite方法,也就是在序列化的过程中,根据入参value 去Map中取出相应值并传递给JsonWriter
  5. TypeAdapterread方法,也就是在反序列化的过程中,根据reader.nextString()值跟Map中的值比较,返回对应值关联的枚举类型。

源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import com.google.gson.Gson
import com.google.gson.TypeAdapter
import com.google.gson.TypeAdapterFactory
import com.google.gson.annotations.SerializedName
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter

class EnumTypeAdapterFactory: TypeAdapterFactory {
    override fun <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
        if (!type.rawType.isEnum) {
            return null
        }

        val maps = mutableMapOf<T, ValueType>()

        type.rawType.enumConstants.filter { it != null }.forEach {
            val tt: T = it!! as T

            val serializedName = tt.javaClass.getField(it.toString()).getAnnotation(SerializedName::class.java)

            if (serializedName != null) {
                maps[tt] = ValueType(serializedName.value, BasicType.STRING)
                return@forEach
            }

            val field = tt.javaClass.declaredFields.firstOrNull { it2 ->
                BasicType.isBasicType(it2.type.name)
            }
            if (field != null) {
                field.isAccessible = true
                val basicType = BasicType.get(field.type.name)
                val value: Any = when (basicType) {
                    BasicType.INT -> field.getInt(tt)
                    BasicType.STRING -> field.get(tt) as String
                    BasicType.LONG -> field.getLong(tt)
                    BasicType.DOUBLE -> field.getDouble(tt)
                    BasicType.BOOLEAN -> field.getBoolean(tt)
                }
                maps[tt] = ValueType(value, basicType)
            } else {
                maps[tt] = ValueType(tt.toString(), BasicType.STRING)
            }
        }

        return object: TypeAdapter<T>() {
            override fun write(out: JsonWriter, value: T?) {
                if (value == null) {
                    out.nullValue()
                } else {
                    val valueType = maps[value]!!
                    when (valueType.type) {
                        BasicType.INT -> out.value(valueType.value as Int)
                        BasicType.STRING -> out.value(valueType.value as String)
                        BasicType.LONG -> out.value(valueType.value as Long)
                        BasicType.DOUBLE -> out.value(valueType.value as Double)
                        BasicType.BOOLEAN -> out.value(valueType.value as Boolean)
                    }
                }
            }

            override fun read(reader: JsonReader): T? {
                if (reader.peek() == JsonToken.NULL) {
                    reader.nextNull()
                    return null
                } else {
                    val source = reader.nextString()
                    var tt: T? = null
                    maps.forEach { value, type ->
                        if (type.value.toString() == source) {
                            tt = value
                            return@forEach
                        }
                    }
                    return tt
                }
            }

        }
    }

    data class ValueType(var value: Any, var type: BasicType)

    enum class BasicType(var value: String) {
        INT("int"),
        STRING("java.lang.String"),
        LONG("long"),
        DOUBLE("double"),
        BOOLEAN("boolean");


        companion object {
            fun isBasicType(name: String): Boolean {
                return values().any { it.value == name }
            }

            fun get(name: String) = values().first { it.value == name }
        }
    }
}


4. 使用

创建一个测试的枚举类:

1
2
3
4
5
6
7
8
import com.google.gson.annotations.SerializedName

enum class TestEnum(var type: Int) {
    @SerializedName("ffff")
    FIRST(5),

    SECOND(6)
}

创建一个测试的数据结构:

1
2
3
data class TestBean(var id: Int,
                    var type: TestEnum,
                    var type2: TestEnum)

测试代码:

1
2
3
4
5
6
7
val gson = GsonBuilder().registerTypeAdapterFactory(EnumTypeAdapterFactory()).create()

val bean = TestBean(3, TestEnum.FIRST, TestEnum.SECOND)
println(gson.toJson(bean))

val str = "{\"id\":3,\"type\":\"ffff\", \"type2\":6}"
print(gson.fromJson(str, TestBean::class.java))

打印结果:

1
2
{"id":3,"type":"ffff","type2":6}
TestBean(id=3, type=FIRST, type2=SECOND)

既不用针对具体的EnumJsonSerializer,也不用入侵Enum类添加注解,只要注册一个EnumTypeAdapterFactory就能够对所有枚举类(针对其他特殊的可修改源码,思路类似)做处理,真是简洁又优雅。

This post is licensed under CC BY 4.0 by the author.