遇到问题
最近在碰到几个奇怪的问题:
- 对接的某第三方文档上的 HTTP 接口返回写的是整数,但是实际上却是字符串。
- 研究百度网盘的接口时,发现其接口返回的某些字段的类型在整数和字符串之间变换。
分析问题
上述问题会影响到 Go 语言中反序列化 JSON 字符串,使得解析报错, 比如运行以下代码:https://go.dev/play/p/kyRfAR8y__a
go
package main
import (
"encoding/json"
"log"
)
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
str := `{"name": "th","age":"100"}`
p := new(Person)
err := json.Unmarshal([]byte(str), p)
if err != nil {
// // json: cannot unmarshal string into Go struct field Person.age of type int
log.Println(err)
}
}
报错:json: cannot unmarshal string into Go struct field Person.age of type int
原因是: Person 结构体中的 age 字段是 int 类型,但是需要反序列化的 JSON 字符串 str 中的 age 是 string 字符串,两者的类型不一致,导致反序列化错误。
在某些弱类型的语言中,比如 PHP,有可能就会出现 int 和 string 相混淆的情况,导致序列化成JSON后,某些字段有可能表示为整形,有时候又是 string。
比如下面的 PHP代码随机输出的JSON中 age 字段类型可能是整形或者string:
php
class Person {
public $name;
public $age;
public function __construct($name, $age) {
$this->name = $name;
$this->age = $age;
}
public function jsonSerialize() {
// 根据 age 的类型进行序列化
if (is_int($this->age)) {
return [
'name' => $this->name,
'age' => $this->age
];
} elseif (is_string($this->age)) {
return [
'name' => $this->name,
'age' => (string)$this->age
];
}
}
}
for ($i = 1; $i <= 10; $i++) {
$name = "Person " . $i;
$age = (random_int(0, 1) === 0) ? random_int(1, 100) : strval(random_int(1, 100));
$person = new Person($name, $age);
$jsonData = json_encode($person, JSON_UNESCAPED_UNICODE);
echo "JSON $i: $jsonData\n";
}
输出:
go
JSON 1: {"name":"Person 1","age":"36"}
JSON 2: {"name":"Person 2","age":24}
JSON 3: {"name":"Person 3","age":19}
JSON 4: {"name":"Person 4","age":94}
JSON 5: {"name":"Person 5","age":40}
JSON 6: {"name":"Person 6","age":54}
JSON 7: {"name":"Person 7","age":"33"}
JSON 8: {"name":"Person 8","age":"57"}
JSON 9: {"name":"Person 9","age":"12"}
JSON 10: {"name":"Person 10","age":9}
解决问题
作为乙方,我们不能要求甲方改变太多,只能去适应环境,所以我们要能够准确解析出字段的值。
方法一:使用官方 json 库的 Number� (推荐)
其实 Number 就是 string,看官方源代码:
php
// A Number represents a JSON number literal.
type Number string
// String returns the literal text of the number.
func (n Number) String() string { return string(n) }
// Float64 returns the number as a float64.
func (n Number) Float64() (float64, error) {
return strconv.ParseFloat(string(n), 64)
}
// Int64 returns the number as an int64.
func (n Number) Int64() (int64, error) {
return strconv.ParseInt(string(n), 10, 64)
}
至于为什么把 Age 字段类型设置为json.Number就能正确解析,是因为 json 库对 Number 类型进行了特殊处理(后面有空再梳理一篇关于此的源代码阅读的文章)
json.Number 的使用:
go
type Person struct {
Name string `json:"name"`
Age json.Number `json:"age"`
}
func main() {
str := `{"name": "th","age":"100"}`
p := new(Person)
err := json.Unmarshal([]byte(str), p)
if err != nil {
log.Println(err)
}
age, err := p.Age.Int64()
if err != nil {
log.Println(err)
}
log.Println(fmt.Sprintf("age:%d", age))
}
注:甚至可以使用 json.RawMessage,代码:
go
// 在 rm 内容为整数(int)或者字符串时,下面代码适用
func getCode(rm json.RawMessage) int {
var intCode int
err := json.Unmarshal(rm, &intCode)
if err != nil {
var stringCode string
err = json.Unmarshal(rm, &stringCode)
if err != nil {
} else {
intCode, _ = strconv.Atoi(stringCode)
}
}
return intCode
}
方法二 使用 interface{} 或者 any
go
type Person struct {
Name string `json:"name"`
Age any `json:"age"`
}
func main() {
str := `{"name": "th","age":"100"}`
p := new(Person)
err := json.Unmarshal([]byte(str), p)
if err != nil {
log.Println(err)
}
if v, ok := p.Age.(int); ok {
log.Printf("c is int, %d", v)
}
if v, ok := p.Age.(string); ok {
log.Printf("c is string, %s", v)
}
}
方法三 *Person 实现方法 UnmarshalJSON
借此,*Person 实现了自己的反序列化方法:
go
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
func (p *Person) UnmarshalJSON(data []byte) error {
// first, unmarshal the data into a map of raw json
var m map[string]json.RawMessage
if err := json.Unmarshal(data, &m); err != nil {
return err
}
p.Name = string(m["name"])
p.Age = getCode(m["age"])
return nil
}
func getCode(rm json.RawMessage) int {
var intCode int
err := json.Unmarshal(rm, &intCode)
if err != nil {
var stringCode string
err = json.Unmarshal(rm, &stringCode)
if err != nil {
} else {
intCode, _ = strconv.Atoi(stringCode)
}
}
return intCode
}
func main() {
str := `{"name": "th","age":"100"}`
p := new(Person)
json.Unmarshal([]byte(str), p)
log.Printf("age:%d\n", p.Age)
}
方法四 使用第三方 json 库处理
使用的是jsoniter,启动模糊模式来支持 PHP 传递过来的 JSON。
go
package main
import (
"log"
jsoniter "github.com/json-iterator/go"
"github.com/json-iterator/go/extra"
)
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
str := `{"name": "th","age":"100"}`
p := new(Person)
extra.RegisterFuzzyDecoders()
err := jsoniter.Unmarshal([]byte(str), p)
if err != nil {
log.Println(err)
}
log.Printf("age:%d\n", p.Age)
}
参考阅读
[1] Golang 中使用 JSON 的小技巧 :https://colobu.com/2017/06/21/json-tricks-in-Go/
[2] golang-利用json-iterator库兼容解析php-json:https://yuerblog.cc/2019/11/08/golang-利用json-iterator库兼容解析php-json/
[3] Go 1.22 源代码:src/encoding/json/decode.go
[4] 使用多值类型和任意数量的键来反序列化 JSON 字符串:https://stackoverflow.com/questions/71516691/unmarshall-json-with-multiple-value-types-and-arbitrary-number-of-keys