Go 反序列化 JSON 中类型不确定的字段

遇到问题

最近在碰到几个奇怪的问题:

  • 对接的某第三方文档上的 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

相关推荐
待磨的钝刨1 小时前
【格式化查看JSON文件】coco的json文件内容都在一行如何按照json格式查看
开发语言·javascript·json
qq_1728055913 小时前
GIN 反向代理功能
后端·golang·go
__AtYou__13 小时前
Golang | Leetcode Golang题解之第535题TinyURL的加密与解密
leetcode·golang·题解
小百菜16 小时前
dom4j实现xml转map,xml转json字符串
xml·json·xml转map·xml转json
yuchangchenTT18 小时前
就是这个样的粗爆,手搓一个计算器:JSON格式化计算器
前端·json·365快速计算器·在线计算器
engchina18 小时前
Python代码解析:处理JSON数据并导入Neo4j数据库
数据库·python·json
kevin_tech18 小时前
Go API 多种响应的规范化处理和简化策略
开发语言·后端·golang·状态模式
幺零九零零21 小时前
【Golang】sql.Null* 类型使用(处理空值和零值)
数据库·sql·golang
cookies_s_s21 小时前
Golang--DOS命令、变量、基本数据类型、标识符
golang
__AtYou__1 天前
Golang | Leetcode Golang题解之第541题反转字符串II
leetcode·golang·题解