在 Golang 中对于 json 的序列化和反序列化的控制大概有三种方式
在默认的 JSON TAG 加上控制标签
实现MarshalJSON() ([]byte, error)和UnmarshalJSON(b []byte) error 覆盖默认的 JSON 序列化和反序列化方法
利用反射解析 Struct 字段自定义实现一个JSON 序列化和反序列化的功能
下面重点看下前两个比较简单的 JSON 序列化和反序列化的方法,第三个通过反射实现的序列化和反序列化的方法比较复杂且适用于自定义需求特别强的场景。
序列化
通过 Golang struct 的 tag 我们可以对例子中的 Student 模型类实现一些个性化的序列化需求
控制一个字段的值是空值的时候,序列化的结果中不包含这个字符串
控制一个字段在序列化的时候直接不被序列化
控制一个字段序列化为其它的格式
控制一个字段在不同的值范围序列化为不同的形式
根据已有的字段序列化输出新的字段
对于前两个需求在 golang 中我们可以简单的通过在 tag 上增加一些标记来实现,分别是`json:",omitempty"` `json:"-"`
下面我们着重来看下后面的几个需求
首先假设我们有一个这样的模型类:
type Student struct {
ID int64 `json:"id"`
Name string `json:"name"`
Class int `json:"class"`
Grade int `json:"grade"`
Age float64 `json:"age"`
}
Go
控制一个字段序列化为其它的格式
现在我们需要将 Student 中的 Age 年龄字段序列化为字符串的形式
我们可以在 Student 上定义 MarshalJSON() ([]byte, error) 方法
由于 Student 上 Age 的字段是 int 但是我们序列化输出的是 string 所以我们必须要定义一个中间 struct 方便序列化 Age 是 string,当然也可以使用 Map 但是过于繁杂,我们需要将一个个字段取出来放进 map 里面。在定义中间 struct 的时候我们为了不重复的的申明多个字段,我们可以匿名组合原有的类型, 在中间类型上将 Age 变为 string 覆盖Student 中的 Age 字段。
func (s *Student) MarshalJSON() ([]byte, error) {
type Temp struct {
Age string `json:"age"`
*Student
}
return json.Marshal(&Temp{
Age: fmt.Sprintf("%d years old", (int)(s.Age)),
Student: s,
})
}
Go
但是此时有一个问题我们在 Student 上定义了一个 MarshalJSON() ([]byte, error) 方法,在这个方法中我们用到了一个中间 struct Temp 他匿名组合了 Student
在 json.Marshal Temp 的过程中会调用 Temp 上的 MarshalJSON 方法,而这个方法就是 Student 上的 MarshalJSON 所以会导致循环调用。
我们可以利用 Golang 中用一个类型定义一个新的类型的办法,定义中间类型 T 这个 T 类型只会继承 Student 的所有字段而不会继承他的方法。这个时候我们在匿名组合定义我们序列化需要的中间类型 Temp
func (s *Student) MarshalJSON() ([]byte, error) {
type T Student
type Temp struct {
Age string `json:"age"`
*T
}
return json.Marshal(&Temp{
Age: fmt.Sprintf("%d years old", (int)(s.Age)),
T: (*T)(s),
})
}
Go
控制一个字段在不同的值范围序列化为不同的形式
这种场景相对较少,我们还是以 Student 中的 Age 字段为例,假设当年龄处于非法值的时候我们不输出这个字段,这里非法值的界定是 age < 0 和 age > 100, 当 age < 10 时我们输出 0 的 float 值。当 age ≥ 10 并且 age < 20 时输出 age 的 int 值,其它正常值输出 age 的整数字符串加上 "years old"。
整体的实现思路和上面类似增加了多个中间 struct 来实现不同的功能
func (s *Student) MarshalJSON() ([]byte, error) {
type T Student
type Temp1 struct {
Age int `json:"age"`
*T
}
type Temp2 struct {
Age *string `json:"age,omitempty"`
*T
}
if s.Age < 0 || s.Age > 100 {
return json.Marshal(&Temp2{
Age: nil,
T: (*T)(s),
})
}
if s.Age < 10 {
return json.Marshal((*T)(s))
}
if s.Age >= 10 && s.Age < 20 {
return json.Marshal(&Temp1{
Age: (int)(s.Age),
T: (*T)(s),
})
}
str := fmt.Sprintf("%d years old", (int)(s.Age))
return json.Marshal(&Temp2{
Age: &str,
T: (*T)(s),
})
}
Go
根据已有的字段序列化输出新的字段
也是对于 Student 这个模型类我们现在需要对里面的 Class 和 Grade 两个字段整合成一个复杂的结果就是学生的年级班级信息。
思路同上我们需要定义中间 struct Temp 在里面定义新的字段 GradeClassInfo
func (s *Student) MarshalJSON() ([]byte, error) {
type T Student
type Temp struct {
GradeClassInfo string `json:"grade_class_info"`
*T
}
return json.Marshal(&Temp{
GradeClassInfo: fmt.Sprintf("grade: %d class: %d", s.Grade, s.Class),
T: (*T)(s),
})
}
Go
如果我们不想看到原有的 grade 和 class 字段的序列化结果可以 json:"-" 屏蔽掉
type Student struct {
ID int64 `json:"id"`
Name string `json:"name"`
Class int `json:"-"`
Grade int `json:"-"`
Age float64 `json:"age"`
}
Go
反序列化:
正常情况我们需要自定义反序列化的情况比较少,但实际开发的过程中还是有一些情况会用到。在 OpenStack 的 API 接口中就有不少这类的情况。我遇到的情况主要有以下种情况:
JSON 字符串和目标字段类型不同
给一个字段在反序列化的时候添加非空默认值
对于一个字段在不同的值会有多个类型
极其不规范的嵌套数据,甚至里面的字符串并非是一个标准的 JSON string
下面我们具体来看下这些情况怎么样去反序列化会比较优雅
JSON 字符串和目标字段类型不同
这事一个非常常见的需求,经常我们定义的 struct 模型和要反序列化的 JSON string 在某些字段上有微小类型差别,大概率是服务端版本问题导致的。
还是以 Student 模型为例实现这个需求我们需要在 Student 上增加一个 UnmarshalJSON(data []byte) error 方法或在他们的某些字段上增加一个 UnmarshalJSON(data []byte) error 方法。
比如现在我们需要对下面的 JSON string 反序列化为 Student 模型
{"id":1,"name":"Ben","age":"7"}
Go
这里 Age 字段在字符串中是 string 类型而在 Student 中是 float64 类型,如果我们想保持现有的业务逻辑不变我们应该怎么进行处理呢?
和上面序列化思路类似的还是需要一个中间的类型代替我们原始的类型去做反序列化操作具体的代码如下:
func (s *Student) UnmarshalJSON(data []byte) error {
type T Student
type Temp struct {
Age string `json:"age"`
*T
}
var t *Temp
err := json.Unmarshal(data, &t)
if err != nil {
return err
}
*s = *(*Student)(t.T)
age, err := strconv.ParseFloat(t.Age, 64)
if err != nil {
return err
}
s.Age = age
return nil
}
Go
这里我们的中间类型 Temp 组合了原始模型 Student 的新类型 T 并且增加了一个 string 类型的 Age 字段覆盖 T 类型中的 float64 类型的 Age 字段。我们在反序列化 Temp struct 后将组合的 T 赋回给 UnmarshalJSON(data []byte) error 方法的 receiver s。注意这里 s 的类型是 Student 的指针类型,如果直接通过 s = (*Student)(t.T) 赋值这里只改变了形参也就是本地变量 s 指向的地址,实际的 Student 变量是不会变化的。所以我们需要修改 s 指向的值为我们新反序列化的内容。最后我们再将 Age 字段转换成浮点数赋值给变量 s。
给一个字段在反序列化的时候添加非空默认值
通常一个字段在序列化的时候的默认值就是其 Golang 里面的默认值也就是零值,比如 int 、float 类型是 0,string 类型是 "", bool 类型是 false,指针类型是 nil。但往往我们在业务开发过程中很多字段会有业务上的默认值而这个默认值很有可能不是其类型的零值。
还是以 Student 模型 struct 为例我们想在反序列化 Name 字段的时候如果解析不到则使用默认值 "Unknown" 而不是空字符串
func (s *Student) UnmarshalJSON(data []byte) error {
type T Student
var t *T
err := json.Unmarshal(data, &t)
if err != nil {
return err
}
*s = *(*Student)(t)
if s.Name == "" {
s.Name = "Unknown"
}
return nil
}
Go
和前面思路一样,只是增加一个字段空值的判断和设置默认值的过程
对于一个字段在不同的值会有多个类型
这种情况比较少见,但往往也是非常恶心的情况。 比如上面 Student 模型中 Age 字段有的情况是 int 类型,有的时候是 string 类型,还有的时候是浮点数类型,也有指针类型的时候。我们需要怎么来进行反序列化这些情况呢。
type ComplexType float64
func (c *ComplexType) UnmarshalJSON(data []byte) error {
s := string(data)
if s == `""` {
return nil
}
if s == "null" {
return nil
}
s = strings.Trim(s, "\"")
v, err := strconv.ParseFloat(s, 64)
if err != nil {
return err
}
*c = ComplexType(v)
return nil
}
func (s *Student) UnmarshalJSON(data []byte) error {
type T Student
type Temp struct {
Age ComplexType `json:"age"`
*T
}
var t *Temp
err := json.Unmarshal(data, &t)
if err != nil {
return err
}
*s = *(*Student)(t.T)
s.Age = float64(t.Age)
return nil
}
Go
这个处理的过程大体思路和原来类似,唯一不一样的在于对与 Age 这个字段我们需要处理它的多种值类型的可能。这里我们需要定义一个复杂类型 Complex 在它上面定义多种值类型反序列化处理的过程。
这种多值情况的另一种比较常见的例子是数字科学记数法的问题: 比如 ID 为 2.585116796E9 这样在默认解析 json 的时候就会报错 json: cannot unmarshal number 2.585116796E9 into Go struct field Temp.id of type int64
此时我们的做法类似:
func (s *Student) UnmarshalJSON(data []byte) error {
type T Student
type Temp struct {
ID interface{} `json:"id"`
*T
}
var t *Temp
d := json.NewDecoder(bytes.NewReader(data))
err := d.Decode(&t)
if err != nil {
return err
}
*s = *(*Student)(t.T)
if id, ok := t.ID.(float64); ok {
s.ID = int64(id)
}
return nil
}
Shell
这里我们在中间临时 struct 中使用 interface{} 来做解析兼容多种类型的情况,这里注意在 golang json 中会将科学记数法的整数类型转为 float64 类型
极其不规范的嵌套数据,甚至里面的字符串并非是一个标准的 JSON string
这个的处理思路总体和上面的情况一样,无非就是在 ComplexType 的 UnmarshalJSON 方法中加入更复杂的处理和提取值的逻辑。
总结:
基本上上类需求都是通过重写 Json 默认的序列化和反序列化方法来进行实现的,我们为了保持兼容性,所以尽量不会修改最终的模型类,而是通过定义新的中间类型来处理复杂的序列化和反序列化需求,把这些逻辑都封装屏蔽在MarshalJSON() ([]byte, error)和UnmarshalJSON(b []byte) error 这两个方法里面。