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 的小技巧 : \[2\] golang-利用json-iterator库兼容解析php-json:[https://yuerblog.cc/2019/11/08/golang-利用json-iterator库兼容解析php-json/](https://yuerblog.cc/2019/11/08/golang-%E5%88%A9%E7%94%A8json-iterator%E5%BA%93%E5%85%BC%E5%AE%B9%E8%A7%A3%E6%9E%90php-json/) \[3\] Go 1.22 源代码:src/encoding/json/decode.go \[4\] 使用多值类型和任意数量的键来反序列化 JSON 字符串:

相关推荐
花酒锄作田8 天前
Gin 框架中的规范响应格式设计与实现
golang·gin
qwfys2009 天前
How to install golang 1.26.0 to Ubuntu 24.04
ubuntu·golang·install
codeejun9 天前
每日一Go-25、Go语言进阶:深入并发模式1
开发语言·后端·golang
石牌桥网管9 天前
Go 泛型(Generics)
服务器·开发语言·golang
上海合宙LuatOS9 天前
LuatOS核心库API——【json 】json 生成和解析库
java·前端·网络·单片机·嵌入式硬件·物联网·json
敲代码的柯基9 天前
一篇文章理解tsconfig.json和vue.config.js
javascript·vue.js·json
小二·9 天前
Go 语言系统编程与云原生开发实战(第21篇)
开发语言·云原生·golang
小二·9 天前
Go 语言系统编程与云原生开发实战(第20篇)
开发语言·云原生·golang
女王大人万岁9 天前
Golang实战Eclipse Paho MQTT库:MQTT通信全解析
服务器·开发语言·后端·golang
codeejun9 天前
每日一Go-24、Go语言实战-综合项目:规划与搭建
开发语言·后端·golang