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

  • 22
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
您好,关于您的问题,Go语言使用标准库json包进行序列序列操作。具体实现方法如下: 1. 对于结构体的序列序列操作 首先定义一个结构体,该结构体字段必须是导出(首字母大写)的,方可被序列序列。 例如: ```go type Person struct { Name string `json:"name"` Age int `json:"age"` } ``` 在定义好结构体之后,即可使用json的Marshal函数进行序列操作,使用Unmarshal函数进行序列操作。 例如: ```go // 序列 p := Person{Name: "Tom", Age: 18} bytes, err := json.Marshal(p) if err != nil { // 处理错误 } fmt.Println(string(bytes)) // {"name":"Tom","age":18} // 序列 var person Person err = json.Unmarshal(bytes, &person) if err != nil { // 处理错误 } fmt.Println(person.Name) // Tom fmt.Println(person.Age) // 18 ``` 2. 对于map、slice等类型序列序列操作 对于map、slice等类型,直接使用json的Marshal函数进行序列操作,使用Unmarshal函数进行序列操作即可。 例如: ```go // map序列 m := map[string]interface{}{ "name": "Tom", "age": 18, } bytes, err := json.Marshal(m) if err != nil { // 处理错误 } fmt.Println(string(bytes)) // {"age":18,"name":"Tom"} // map序列 var data map[string]interface{} err = json.Unmarshal(bytes, &data) if err != nil { // 处理错误 } fmt.Println(data["name"]) // Tom fmt.Println(data["age"]) // 18 // slice序列 s := []string{"hello", "world"} bytes, err := json.Marshal(s) if err != nil { // 处理错误 } fmt.Println(string(bytes)) // ["hello","world"] // slice序列 var data []string err = json.Unmarshal(bytes, &data) if err != nil { // 处理错误 } fmt.Println(data[0]) // hello fmt.Println(data[1]) // world ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值