前往:输入错误的JSON反序列化,或如何解决API开发人员错误

图片

最近,我碰巧在Go上开发了一个http客户端,以提供一种服务,该服务提供REST API并以json作为编码格式。这是一项标准任务,但是在工作过程中,我不得不面对一个非标准问题。我告诉你重点是什么。

如您所知,json格式具有数据类型。四个基元:字符串,数字,布尔值,null;和两种结构类型:对象和数组。在这种情况下,我们对原始类型感兴趣。这是一个示例json代码,其中包含四个不同类型的字段:

{
	"name":"qwerty",
	"price":258.25,
	"active":true,
	"description":null,
}

如示例所示,字符串值用引号引起来。数字-没有引号。布尔类型只能具有以下两个值之一:true或false(不带引号)。空类型因此也为空(也没有引号)。

现在是问题本身。在某个时候,在详细检查从第三方服务接收到的json代码时,我发现其中一个字段(我们称其为price)除了数字值外还定期具有字符串值(带引号的数字)。也就是说,具有不同参数的相同查询可以返回数字作为数字,也可以返回相同的数字作为字符串。我无法想象在另一端如何组织返回此类结果的代码,但这显然是由于该服务本身是一个聚合器并从不同来源提取数据,并且开发人员没有将服务器响应json转换为单一格式。尽管如此,还是有必要配合实际工作。

但是后来我更加惊讶。除了true和false外,逻辑字段(我们称其为active)还返回字符串值``true'',``false'',甚至返回数字1和0(分别为true和false)。

如果我要在弱类型的PHP中处理json语句,那么对数据类型的所有这些混淆就不会变得很关键,但是Go具有强类型,并且需要清楚地指示反序列化字段的类型。结果,需要实现一种机制,该机制允许在反序列化过程中将活动字段的所有值转换为逻辑类型,并将价格字段的任何值转换为数字值。

让我们从价格字段开始。

假设我们有这样的json代码:

[
	{"id":1,"price":2.58},
	{"id":2,"price":7.15}
]

也就是说,json包含具有两个数字类型字段的对象数组。Go上此json的标准反序列化代码如下所示:

type Target struct {
	Id    int     `json:"id"`
	Price float64 `json:"price"`
}

func main() {
	jsonString := `[{"id":1,"price":2.58},
					{"id":4,"price":7.15}]`

	targets := []Target{}

	err := json.Unmarshal([]byte(jsonString), &targets)
	if err != nil {
		fmt.Println(err)
		return
	}

	for _, t := range targets {
		fmt.Println(t.Id, "-", t.Price)
	}
}

在此代码中,我们将id字段反序列化为int,将price字段反序列化为float64。现在假设我们的json代码如下所示:

[
	{"id":1,"price":2.58},
	{"id":2,"price":"2.58"},
	{"id":3,"price":7.15},
	{"id":4,"price":"7.15"}
]

也就是说,价格字段包含数字类型和字符串的值。在这种情况下,只能将价格字段的数值解码为float64类型,而字符串值将导致有关类型不兼容的错误。这意味着float64和其他任何原始类型都不适合对该字段进行反序列化,并且我们需要具有自己的反序列化逻辑的自定义类型。

作为这种类型,请声明一个带有单个float64类型的Float64字段的CustomFloat64结构。

type CustomFloat64 struct{
	Float64 float64
}

并立即在“目标”结构的“价格”字段中指明此类型:

type Target struct {
	Id    int           `json:"id"`
	Price CustomFloat64 `json:"price"`
}

现在,您需要描述自己的逻辑来解码CustomFloat64类型的字段。

“ encoding / json”包具有两个特殊方法:MarshalJSONUnmarshalJSON,它们用于定制特定用户数据类型的编码和解码逻辑。覆盖这些方法并描述您自己的实现就足够了。

对任意类型的CustomFloat64覆盖UnmarshalJSON方法。在这种情况下,必须严格遵循该方法的签名,否则它将根本无法工作,最重要的是,它不会产生错误。

func (cf *CustomFloat64) UnmarshalJSON(data []byte) error {

在输入时,此方法采用一片字节(数据),其中包含已解码json的特定字段的值。如果将字节序列转换为字符串,那么我们将以json形式正确地看到字段的值。也就是说,如果它是字符串类型,那么我们将确切地看到带双引号的字符串(“ 258”),如果它是数字类型,那么我们将看到不带引号的字符串(258)。

为了将数字值与字符串值区分开,必须检查第一个字符是否为引号。由于UNICODE表中的双引号字符占用一个字节,因此我们只需要通过将数据切片的第一个字节与UNICODE中的字符数进行比较来检查它。这是数字34。请注意,字符通常不等于一个字节,因为它可能占用多个字节。 Go中的符号等同于符文(rune)。在我们的情况下,此条件已足够:

if data[0] == 34 {

如果满足条件,则该值具有字符串类型,我们需要获取引号之间的字符串,即第一个字节与最后一个字节之间的切片字节。该分片包含一个可以解码为原始类型float64的数值。这意味着我们可以对其应用json.Unmarshal方法,同时将结果保存在Float64字段的CustomFloat64结构中。

err := json.Unmarshal(data[1:len(data)-1], &cf.Float64)

如果数据切片不以引号开头,则它已经包含数字数据类型,我们可以将json.Unmarshal方法直接应用于整个数据切片。

err := json.Unmarshal(data, &cf.Float64)

这是UnmarshalJSON方法的完整代码:

func (cf *CustomFloat64) UnmarshalJSON(data []byte) error {
	if data[0] == 34 {
		err := json.Unmarshal(data[1:len(data)-1], &cf.Float64)
		if err != nil {
			return errors.New("CustomFloat64: UnmarshalJSON: " + err.Error())
		}
	} else {
		err := json.Unmarshal(data, &cf.Float64)
		if err != nil {
			return errors.New("CustomFloat64: UnmarshalJSON: " + err.Error())
		}
	}
	return nil
}

结果,对我们的json代码使用json.Unmarshal方法,price字段的所有值将对我们透明地转换为基本类型float64,并将结果写入CustomFloat64结构的Float64字段。

现在我们可能需要将Target结构转换回json。但是,如果我们将json.Marshal方法直接应用于CustomFloat64类型,则我们将此结构序列化为一个对象。我们需要将价格字段编码为数值。为了自定义自定义类型CustomFloat64的编码逻辑,我们为它实现MarshalJSON方法,同时严格遵守方法签名:

func (cf CustomFloat64) MarshalJSON() ([]byte, error) {
	json, err := json.Marshal(cf.Float64)
	return json, err
}

在此方法中,您需要做的就是再次使用json.Marshal方法,但已将其不仅应用于CustomFloat64结构,而且应用于其Float64字段。从方法中,我们返回接收到的字节片和错误。

这是显示序列化和反序列化结果的完整代码(为简便起见,省略了错误检查,带有双引号符号的字节数为常数):

package main

import (
	"encoding/json"
	"errors"
	"fmt"
)

type CustomFloat64 struct {
	Float64 float64
}

const QUOTES_BYTE = 34

func (cf *CustomFloat64) UnmarshalJSON(data []byte) error {
	if data[0] == QUOTES_BYTE {
		err := json.Unmarshal(data[1:len(data)-1], &cf.Float64)
		if err != nil {
			return errors.New("CustomFloat64: UnmarshalJSON: " + err.Error())
		}
	} else {
		err := json.Unmarshal(data, &cf.Float64)
		if err != nil {
			return errors.New("CustomFloat64: UnmarshalJSON: " + err.Error())
		}
	}
	return nil
}

func (cf CustomFloat64) MarshalJSON() ([]byte, error) {
	json, err := json.Marshal(cf.Float64)
	return json, err
}

type Target struct {
	Id    int           `json:"id"`
	Price CustomFloat64 `json:"price"`
}

func main() {
	jsonString := `[{"id":1,"price":2.58},
					{"id":2,"price":"2.58"},
					{"id":3,"price":7.15},
					{"id":4,"price":"7.15"}]`

	targets := []Target{}

	_ := json.Unmarshal([]byte(jsonString), &targets)

	for _, t := range targets {
		fmt.Println(t.Id, "-", t.Price.Float64)
	}

	jsonStringNew, _ := json.Marshal(targets)
	fmt.Println(string(jsonStringNew))
}

代码执行结果:

1 - 2.58
2 - 2.58
3 - 7.15
4 - 7.15
[{"id":1,"price":2.58},{"id":2,"price":2.58},{"id":3,"price":7.15},{"id":4,"price":7.15}]

让我们继续第二部分,并在逻辑字段的值不一致的情况下为json反序列化实现相同的代码。

假设我们有这样的json代码:

[
	{"id":1,"active":true},
	{"id":2,"active":"true"},
	{"id":3,"active":"1"},
	{"id":4,"active":1},
	{"id":5,"active":false},
	{"id":6,"active":"false"},
	{"id":7,"active":"0"},
	{"id":8,"active":0},
	{"id":9,"active":""}
]

在这种情况下,活动字段表示逻辑类型,并且仅存在以下两个值之一:true和false。在反序列化期间,非布尔值将需要转换为布尔值。

在当前示例中,我们接受以下匹配。真值对应于:true(逻辑),“ true”(字符串),“ 1”(字符串),1(数字)。false值对应于:false(逻辑),false(字符串),0(字符串),0(数字),“”(空字符串)。

首先,我们将声明反序列化的目标结构。作为活动字段的类型,我们立即指定自定义类型CustomBool:

type Target struct {
	Id     int        `json:"id"`
	Active CustomBool `json:"active"`
}

CustomBool是一种结构,具有一个bool类型的单个bool字段:

type CustomBool struct {
	Bool bool
}

我们为此结构实现了UnmarshalJSON方法。我将立即给出代码:

func (cb *CustomBool) UnmarshalJSON(data []byte) error {
	switch string(data) {
	case `"true"`, `true`, `"1"`, `1`:
		cb.Bool = true
		return nil
	case `"false"`, `false`, `"0"`, `0`, `""`:
		cb.Bool = false
		return nil
	default:
		return errors.New("CustomBool: parsing \"" + string(data) + "\": unknown value")
	}
}

由于本例中的活动字段具有有限数量的值,因此我们可以使用switch-case构造来决定CustomBool结构的Bool字段的值应等于什么。要检查,只需要两个大小写块。在第一个块中,我们检查值为true,在第二个块中为false。

在编写可能的值时,应注意碎石的作用(这是键上的引号,在英语布局中为字母E)。此字符使您可以在字符串中转义双引号。为了清楚起见,我用带引号和不带引号的符号将值构成框架。因此,“ false”对应于字符串false(不带引号,在json中键入bool),而“ false”对应于字符串“ false”(带引号,在json中键入string)。相同的是值``1''和``1''。第一个是数字1(在JSON中写有引号),第二个是字符串``1''(在JSON中写有引号)。此项“”是一个空字符串,即json格式,它看起来像这样:“”。

相应的值(正确或错误)直接写入CustomBool结构的Bool字段:

cb.Bool = true

在默认值块中,我们返回一个错误,指出该字段具有未知值:

return errors.New("CustomBool: parsing \"" + string(data) + "\": unknown value")

现在我们可以将json.Unmarshal方法应用于我们的json代码,活动字段的值将被转换为基本类型bool。

我们为CustomBool结构实现MarshalJSON方法:

func (cb CustomBool) MarshalJSON() ([]byte, error) {
	json, err := json.Marshal(cb.Bool)
	return json, err
}

这里没有新内容。该方法序列化CustomBool结构的Bool字段。

这是显示序列化和反序列化结果的完整代码(为简便起见,省略了错误检查):

package main

import (
	"encoding/json"
	"errors"
	"fmt"
)

type CustomBool struct {
	Bool bool
}

func (cb *CustomBool) UnmarshalJSON(data []byte) error {
	switch string(data) {
	case `"true"`, `true`, `"1"`, `1`:
		cb.Bool = true
		return nil
	case `"false"`, `false`, `"0"`, `0`, `""`:
		cb.Bool = false
		return nil
	default:
		return errors.New("CustomBool: parsing \"" + string(data) + "\": unknown value")
	}
}

func (cb CustomBool) MarshalJSON() ([]byte, error) {
	json, err := json.Marshal(cb.Bool)
	return json, err
}

type Target struct {
	Id     int        `json:"id"`
	Active CustomBool `json:"active"`
}

func main() {
	jsonString := `[{"id":1,"active":true},
					{"id":2,"active":"true"},
					{"id":3,"active":"1"},
					{"id":4,"active":1},
					{"id":5,"active":false},
					{"id":6,"active":"false"},
					{"id":7,"active":"0"},
					{"id":8,"active":0},
					{"id":9,"active":""}]`

	targets := []Target{}

	_ = json.Unmarshal([]byte(jsonString), &targets)

	for _, t := range targets {
		fmt.Println(t.Id, "-", t.Active.Bool)
	}

	jsonStringNew, _ := json.Marshal(targets)
	fmt.Println(string(jsonStringNew))
}

代码执行结果:

1 - true
2 - true
3 - true
4 - true
5 - false
6 - false
7 - false
8 - false
9 - false
[{"id":1,"active":true},{"id":2,"active":true},{"id":3,"active":true},{"id":4,"active":true},{"id":5,"active":false},{"id":6,"active":false},{"id":7,"active":false},{"id":8,"active":false},{"id":9,"active":false}]

发现


首先。通过为任意数据类型覆盖MarshalJSON和UnmarshalJSON方法,您可以自定义特定json代码字段的序列化和反序列化。除了指定的用例之外,这些函数还用于可空字段。

其次。json文本编码格式是一种广泛使用的信息交换工具,与其他格式相比,它的优点之一是数据类型的可用性。必须严格监控与这些类型的符合性。

All Articles