用golang解析json裡的數字欄位

Reading time ~4 minutes

在寫網路相關應用的時候, 應該常常會碰到需要去解析JSON格式的資料, 而Go在這邊也已經內建了一個蠻方便的套件 - “encoding/json” 可以讓我們輕易地來處理這類型的資料

先來看看下面這段範例:

package main

import (
	"fmt"
	"encoding/json"
)

type Sample struct {
   Name string
   Age int 
}

const sampleData = `{
   "name":"julian",
   "age": -1
}`

func main() {
	var sample Sample
	err := json.Unmarshal([]byte(sampleData), &sample)
	
	if err != nil {
	   panic(err)
	}
	fmt.Println(sample)
}

[執行]

從這個範例可以看到, 我們可以用很簡單的程式碼, 把下面這段JSON內容給對應到Sample這個結構裡面

{
   "name":"julian",
   "age": -1
}

但這邊其實有一個問題, 如果你把這個JSON資料, 改成下面這樣子:

{
   "name":"julian",
   "age": "-1"
}

這在現實世界應該蠻常看到的, 只是多加個雙引號而已, 大家應該也會預期這邊應該也會沒問題的解析出一樣的結果吧? 但你如果實際改了資料執行看看, 你得到的結果應該會是:

panic: json: cannot unmarshal string into Go struct field Sample.Age of type int

goroutine 1 [running]:
main.main()
	/tmp/sandbox247755092/prog.go:23 +0x162

這其實是 “encoding/json” 把雙引號的內容都當作字串來看, 所以當我要把它塞到一個 int 欄位時, 就會出事了

解決方法有好幾種, 下面就一一來看看:

用Field tag來解決

如果把Sample的定義改成下面這樣:

type Sample struct {
   Name string
   Age int `json:",string"`
}

[執行範例]

喔耶, 沒問題了耶!!!可以正常的解出資料了耶!! 慢著, 先別高興太早!! 再試試把雙引號拿掉看看(參考這個範例)

呃, 是怎樣啦!! 換成這個錯誤了!!

panic: json: invalid use of ,string struct tag, trying to unmarshal unquoted value into int

goroutine 1 [running]:
main.main()
	/tmp/sandbox912379425/prog.go:23 +0x162

在現實案例中, 的確是有可能碰到有時送來的資料有雙引號, 有時候沒有, 這方法是沒法一次滿足的

利用Unmarshaler界面來解決

“encoding/json” 是可以讓開發者自行指定怎去解析JSON內容的, 只需要定義一個自定義的型別並實做Unmarshaler界面就可以了, 為了解決這個問題, 我們可以定義一個新的MyInt的型別, 並幫它實做UnmarshalJSON的方法, 參考下面範例:

type MyInt int

func (m *MyInt) UnmarshalJSON(data []byte) error {
	str := string(data)
	
	if unquoted, err := strconv.Unquote(str); err == nil {
	   str = unquoted
	}
	
	result, err := strconv.Atoi(str)
	if err != nil {
		return err
	}
	*m = MyInt(result)
	return nil
}

type Sample struct {
	Name string
	Age  MyInt
}

[執行範例]

在Sample這個結構中的Age欄位, 從原本的int改成MyInt, 這樣json.Unmarshal碰到Age這欄位的話, 就會用MyIntUnmarshalJSON方法去解析

這方法是麻煩了點, 而且可能針對不同型別要去個別做, 但卻是可以同時處理掉前述兩種型態的資料, 程式沒那好看就是了

使用json.Number

“encoding/json” 裡其實還提供一個資料型態json.Number, 這個應該是這個問題的比較正規的解法了, 把Sample的定義改成下面這樣:

type Sample struct {
	Name string
	Age  json.Number
}

[執行範例]

這方法也是可以無誤的解析兩種的型態, 然後當你要取用Age這欄位的int型態時, 你可以用sample.Age.Int64()去取得, 要多一層是麻煩點

那它是怎做到的呢? 來看一下它的原始碼:

// 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)
}

會發現, 其實沒啥了不起的, Number本身就是一個string, 所以它是當字串在處理, 而當你需要數字表示(int或float)時, 再呼叫ParseInt或ParseFloat來處理

使用 Jsoniter 來取代 “encoding/json”

Jsoniter是一個號稱比原生的***“encoding/json”***效能還要來的更好的JSON處理套件, 除了Go的版本外, 它也有Java的版本

效能不在這邊的討論範圍, 但除效能外, 它也是最簡單解決這問題的方法, 先來看看完整的程式碼吧:

package main

import (
	"fmt"
	jsoniter "github.com/json-iterator/go"
	extra "github.com/json-iterator/go/extra"
)

type Sample struct {
	Name string
	Age  int
}

const sampleData = `{
   "name":"julian",
   "age": 45
}`

const sampleData2 = `{
   "name":"julian",
   "age": "45"
}`

func main() {
	extra.RegisterFuzzyDecoders()
	var sample Sample
	err := jsoniter.Unmarshal([]byte(sampleData), &sample)

	if err != nil {
		panic(err)
	}
	fmt.Println(sample)

	err = jsoniter.Unmarshal([]byte(sampleData2), &sample)

	if err != nil {
		panic(err)
	}
	fmt.Println(sample)

}

[執行範例]

為啥說是最簡單的方法呢? 首先, 它有跟 “encoding/json” 完全一樣的使用方法, 把套件換掉後, 程式幾乎一樣, 所以也不需要改啥程式, 但如果只有改這樣, 會發現問題都還是在, 並沒有解決掉, 這時候就要帶入它額外的功能 FuzzyDecoders 了, FuzzyDecoders是在它額外的套件裡面, 所以只需要加入import就可以用了

import extra "github.com/json-iterator/go/extra"

然後在開始使用前, 註冊 FuzzyDecoders 即可(extra.RegisterFuzzyDecoders()), 這樣, 不管有沒雙引號都不會有問題了!!