最近在用 Go 写一个对接某个 API 的 Adapter 时碰到了一点关于 json 反序列化的问题。对方通过 websocket 传递 json 来进行推送消息,但是推送的格式不太统一,而且种类繁多,难以使用少量通用的结构体类型来对这些消息进行反序列化。不过好在这些消息中都有一个共同的 type 字段用于标识该 json 对象的类型,不至于手足无措。在经过几天的资料查询参考后,通过一些解析手段和反射完成了对推送消息的解析,做记录如下。

动态 JSON 解析

对于大量格式不同的 json 对象,最为简单粗暴的方法当然就是用万能的 interface {} 去进行解析,再复杂的 json 对象也能解析成简单的基本类型组合。这样子做虽然在解析的时候舒服了不少,但是后续访问对象字段就麻烦了,需要不断使用字典之类的访问方法对其进行访问,而且 interface {} 类型对 IDE 的智能提示也非常不友好,后续的维护也更为繁琐,容易出问题。以上问题一叠加,interface {} 自然是下下策。那么有什么办法来在运行时根据 json 对象自动选择相应类型进行序列化呢?

延迟解析

一个比较好的方法是使用 json.RawMessage 类型来延后解析。json.RawMessagejson 标准库中提供的一个类型,预先被定义为这个类型的字段在 json.Unmarshal 时,json 库将不会对这个字段进行递归解析,而是将这个字段对应的 raw json 数据存入该字段,并可用于后续解析。

下面用一个简单的例子来说明这个方法,假设我们的接口会返回如下两种格式的 json

// Fruit json
{
    "type": "fruit",
    "data": {
        "name": "orange",
        "color": "red",
        "taste": "sweet"
    }
}
//vegetable json
{
    "type": "vegetable",
    "data": {
        "name": "corn",
        "size": "big",
        "juicy": false
    }
}

以上两种格式的 json 的 data 字段下存放的 json 对象格式不同,而 data 字段所存放的 json 格式由 type 字段规定。这就给了我们一个思路,在首次解析中保留 dataRawMessage 数据,然后通过解析出来的 type 字段值来确定使用哪个静态类型对 data 字段的 RawMessage 进行解析。解析的代码可能看起来像这个样子:

type message struct {
	Type string          `json:"type"`
	Data json.RawMessage `json:"data"`
}
type fruit struct {
	Name  string `json:"name"`
	Color string `json:"color"`
	Taste string `json:"taste"`
}
type vegetable struct {
	Name  string `json:"name"`
	Size  string `json:"size"`
	Juicy bool   `json:"juicy"`
}

func main () {
	rawMsg := `{
		"type": "fruit",
		"data": {
			"name": "orange",
			"color": "red",
			"taste": "sweet"
		}
	}`
	msg := message {}
	json.Unmarshal ([] byte (rawMsg), &msg)
	switch msg.Type {
	case "fruit":
		data := fruit {}
		json.Unmarshal (msg.Data, &data)
		// Additional operations
	case "vegetable":
		data := vegetable {}
		json.Unmarshal (msg.Data, &data)
		// Additional operations
	}
}

在上面这段代码中,我们首先将完整的 json 数据解析为 message 类型,这时候 jsontype 字段被解析到 messageType 字段中,而 jsondata 字段则以未解析的形式放入 messageData 字段中。在下一步中,我们通过对 message.Type 进行 switch 的方式来选择使用哪个静态类型来对 data 进行解析。

二次解析

延迟解析的方法对于那些将会动态变化的部分都存放在一个固定字段下的 json 对象十分有效,但对于那些将动态部分置于和 type 字段同级的 json 对象便无计可施了,因为并没有一个固定的字段用来存储 json.RawMessage。对于这一类格式的 json 对象,一个比较直觉的方法是对其进行二次解析,通过先解析出固有的 type 字段,再根据 type 字段选取合适的静态类型进行解析。

下面用一个简单的例子来说明这个方法,假设我们的接口会返回如下数据:

// Fruit json
{
    "type": "fruit",
    "name": "orange",
    "color": "red",
    "taste": "sweet"
}
//vegetable json
{
    "type": "vegetable",
    "name": "corn",
    "size": "big",
    "juicy": false
}

显然,对于以上 json 对象,我们不能用一个简单的 data 字段来存储 colortastesizejuicy 这四个字段,自然无法延后解析,所以此处我们采用二次解析的方式来解决这个问题。

type message struct {
	Type string `json:"type"`
}
type fruit struct {
	Name  string `json:"name"`
	Color string `json:"color"`
	Taste string `json:"taste"`
}
type vegetable struct {
	Name  string `json:"name"`
	Size  string `json:"size"`
	Juicy bool   `json:"juicy"`
}

func main () {
	rawMsg := `{
		"type":"fruit",
		"name":"orange",
		"color":"red",
		"taste":"sweet"
	 }`
	msg := message {}
	json.Unmarshal ([] byte (rawMsg), &msg)
	switch msg.Type {
	case "fruit":
		data := fruit {}
		json.Unmarshal ([] byte (rawMsg), &data)
		// Additional operations
	case "vegetable":
		data := vegetable {}
		json.Unmarshal ([] byte (rawMsg), &data)
		// Additional operations
	}
}

代码上的区别不算太大,不同之处在于,由于 message 类型现在只用来解析出 type 字段指导类型选取,因此 message 类型中只有一个 type 字段。在根据 rawMsg 解析出类型之后,再将 rawMsg 解析为具体的静态类型。

这两种方法在实际应用中都能比较好地解决动态 json 对象的解析问题。但对于有多层都会发生变化的动态 json 对象,就需要考虑如何嵌套使用这两种方法的问题了,代码也会变得复杂起来。所以还是要尽量避免单一接口返回动态 json 对象的情况出现。

动态选取类型

上面的解析部分主要讲述的是对于动态 json 类型的解析,我们在最后通过一个 switch 表达式完成对具体类型的选取。这样的方式在可用类型较少的时候不会有太大问题,但如果有大量的类型需要选择的话,switch 表达式将会变得又臭又长。有没有什么方法自动根据 type 字段来选取对应的静态类型呢?

反射

对于以上需求,我们要求编程语言能够提供一个机制,来根据给定的 string 创建对应的类型。Go 通过 reflect 包提供了一个反射机制,来允许在运行时动态创建指定类型。具体的细节比较多,有兴趣的朋友可以自行查阅相关资料。

类型注册中心

Go 的 reflect 包提供了 reflect.Type 用来表示一个类型,同时可以通过 reflect.TypeOf () 函数来在运行时获取一个变量的类型信息。通过给定一个 reflect.Type 变量我们能够使用 reflect.New () 函数来创建出指定的变量,那么运行时创建指定类型的变量的问题就解决了,还剩下如何通过 string 获取 reflect.Type 这个问题。

由于 Go 语言是静态语言,没有提供一个全局的类型注册中心,也就是没有在语言机制中提供从 stringreflect.Type 的映射。不过这一点可以通过自行建立映射解决,我们可以用一个 map [string] reflect.Type 来记录映射,在 init 时事先对该 map 进行填充,后面就可以通过这个 map 来获取对应的 reflect.Type,进而动态创建出指定类型了。

示例

下面还是通过上述的例子来展示一下如何利用反射动态创建类型:

type message struct {
	Type string `json:"type"`
}
type fruit struct {
	Name  string `json:"name"`
	Color string `json:"color"`
	Taste string `json:"taste"`
}
type vegetable struct {
	Name  string `json:"name"`
	Size  string `json:"size"`
	Juicy bool   `json:"juicy"`
}

func main () {
	rawMsg := `{
		"type":"fruit",
		"name":"orange",
		"color":"red",
		"taste":"sweet"
	}`
	// 创建注册中心 registry
	registry := make (map [string] reflect.Type)
	// 通过转换 nil 类型指针来将 fruit 类型写入 fruitType 变量
	fruitType := reflect.TypeOf ((*fruit)(nil)).Elem ()
	// 将 fruit 字符串映射到 fruit 类型
	registry ["fruit"] = fruitType

	// 解析 type
	msg := message {}
	json.Unmarshal ([] byte (rawMsg), &msg)

	// 通过 type 查询 registry,获取类型
	t := registry [msg.Type]
	// 创建一个指定类型的变量,并将其指针转换为 interface {} 后写入 data
	data := reflect.New (t).Interface ()
	// 通过 data 指针进行反序列化,此时 json 库能够正确识别 data 的类型并进行解析
	json.Unmarshal ([] byte (rawMsg), data)
	// 打印检查
	fmt.Printf ("%+v", data)
}

上述代码的执行结果如下:

$ go run .\main.go
&{Name:orange Color:red Taste:sweet}

可以看到,代码已经正确解析出了结果。

总结

虽然如上代码正确解析出了结果,但由于 data 变量为 interface {} 类型变量,在访问其字段时仍然需要进行类型断言,需要注意。由于在笔者的项目中 json 解析结果是作为 interface 传递,所以使用反射能够很好地减少代码行数。假如 json 数据解析完是就地使用的话,可能还是使用 switch 更为方便一些。