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

相关推荐
qq_172805592 小时前
GO GIN 推荐的库
开发语言·golang·gin
=(^.^)=哈哈哈3 小时前
Golang如何优雅的退出程序
开发语言·golang·xcode
白总Server8 小时前
MongoDB解说
开发语言·数据库·后端·mongodb·golang·rust·php
Snowbowღ8 小时前
OpenAI / GPT-4o:Python 返回结构化 / JSON 输出
python·json·openai·api·gpt-4o·pydantic·结构化输出
林戈的IT生涯9 小时前
MySQL5.7中增加的JSON特性的处理方法JSON_EXTRACT和JSON_ARRAY_APPEND以及MYSQL中JSON操作的方法大全
json·mysql5.7·json特性·json_extract·mysql中json操作方法
敲代码不忘补水10 小时前
Python 项目实践:简单的计算器
开发语言·python·json·项目实践
liupenglove12 小时前
golang操作mysql利器-gorm
mysql·golang
Rookie_explorers13 小时前
Linux下go环境安装、环境配置并执行第一个go程序
linux·运维·golang
做技术的Pandaer13 小时前
Go 第二期
开发语言·golang
wn53114 小时前
【Go - 类型断言】
服务器·开发语言·后端·golang