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

遇到问题

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

  • 对接的某第三方文档上的 HTTP 接口返回写的是整数,但是实际上却是字符串。
  • 研究百度网盘的接口时,发现其接口返回的某些字段的类型在整数和字符串之间变换。

分析问题

上述问题会影响到 Go 语言中反序列化 JSON 字符串,使得解析报错, 比如运行以下代码:https://go.dev/play/p/kyRfAR8y__a

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:


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";
}

输出:

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,看官方源代码:

// 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 的使用:

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,代码:

// 在 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

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 实现了自己的反序列化方法:

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。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值