golang jsoniter extension 处理动态字段

时间:2022-11-11 01:23:33

1. 背景

golang 原生 json 包,在处理 json 对象的字段的时候,是需要严格匹配类型的。但是,实际上,当我们与一些老系统或者脚本语言的系统对接的时候,有时候需要对类型需要做一下兼容,假设我们有以下需求

目标类型 输入 解析后
int int, string 123, “123” 123
string int, string 123, “123” “123”
time unix_seconds, RFC3339 1680676884, “2023-04-05T14:41:24Z”, “2023-04-05T14:41:24Z”

2. 可选项

我们以 time 作为一个样例

  • 包装类,然后重新实现 Unmarshal 接口

    type MyTime struct {
    	t    time.Time
    }
    

    功能可以实现,但是如果使用的地方很多的情况下,就可能要改动多处,而且,这是全局级别的,可能会影响到很多包的行为

  • 使用 jsonter 的 extension 实现
    jsoniter 的插件文档参考
    我们使用实例级别的 extension, 而非全局,可以针对不同业务逻辑有所区分

    package main
    
    import (
    	"fmt"
    	"reflect"
    	"strconv"
    	"time"
    	"unsafe"
    
    	jsoniter "github.com/json-iterator/go"
    	"github.com/modern-go/reflect2"
    )
    
    type sampleExtension struct {
    	jsoniter.DummyExtension
    }
    
    type wrapEncoder struct {
    	encodeFunc  func(ptr unsafe.Pointer, stream *jsoniter.Stream)
    	isEmptyFunc func(ptr unsafe.Pointer) bool
    	decodeFunc  func(ptr unsafe.Pointer, iter *jsoniter.Iterator)
    }
    
    func (enc *wrapEncoder) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
    	enc.encodeFunc(ptr, stream)
    }
    
    func (codec *wrapEncoder) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
    	codec.decodeFunc(ptr, iter)
    }
    
    func (enc *wrapEncoder) IsEmpty(ptr unsafe.Pointer) bool {
    	if enc.isEmptyFunc == nil {
    		return false
    	}
    
    	return enc.isEmptyFunc(ptr)
    }
    
    // 这里统一改用 unix seconds 进行输出
    func (e *sampleExtension) CreateEncoder(typ reflect2.Type) jsoniter.ValEncoder {
    	if typ.Kind() == reflect.Struct && typ.Type1().PkgPath() == "time" && typ.String() == "time.Time" {
    
    		return &wrapEncoder{
    			func(ptr unsafe.Pointer, stream *jsoniter.Stream) {
    				t := *(*time.Time)(ptr)
    				data := strconv.Itoa(int(t.Unix()))
    				stream.WriteRaw(data)
    			},
    			nil,
    			nil,
    		}
    	}
    
    	return nil
    }
    
    func (e *sampleExtension) CreateDecoder(typ reflect2.Type) jsoniter.ValDecoder {
    	if typ.Kind() == reflect.Struct && typ.Type1().PkgPath() == "time" && typ.String() == "time.Time" {
    		return &wrapEncoder{
    			decodeFunc: func(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
    				switch iter.WhatIsNext() {
    				case jsoniter.NumberValue: // 兼容 unix 数字解析
    					timeUnix := iter.ReadInt()
    					newTime := time.Unix(int64(timeUnix), 0)
    					*(*time.Time)(ptr) = newTime
    
    				case jsoniter.NilValue:
    					iter.Skip()
    
    				case jsoniter.StringValue:
    					timeStr := iter.ReadString()
    					newTime, err := time.Parse(time.RFC3339, timeStr)
    					if err != nil {
    						fmt.Println("Unmarshal err", err)
    					}
    					*(*time.Time)(ptr) = newTime
    
    				}
    			},
    		}
    	}
    
    	return nil
    }
    
    type Person struct {
    	Birth time.Time `json:"birth"`
    }
    
    func main() {
    	extension := &sampleExtension{}
    	jsoniterAPI := jsoniter.Config{}.Froze()
    	jsoniterAPI.RegisterExtension(extension)
    	var p1 = Person{
    		Birth: time.Now(),
    	}
    	j, err := jsoniterAPI.MarshalToString(p1)
    	if err != nil {
    		panic(err)
    	}
    	fmt.Println(j)
    
    	var p2 Person
    	err = jsoniterAPI.Unmarshal([]byte(`{"birth": 1680254527}`), &p2)
    	if err != nil {
    		panic(err)
    	}
    	fmt.Println("p2", p2)
    
    	var p3 Person
    	err = jsoniterAPI.Unmarshal([]byte(`{"birth": "2023-03-21T07:20:04+00:00"}`), &p3)
    	if err != nil {
    		panic(err)
    	}
    	fmt.Println("p3", p3)
    
    	var p4 Person
    	err = jsoniterAPI.Unmarshal([]byte(`{"birth": null}`), &p4)
    	if err != nil {
    		panic(err)
    	}
    	fmt.Println("p4", p4)
    }
    

我们在例子中,实现了:

  • 把 p1 使用了 unix 数字进行序列化
  • 在反序列化 p2/p3/p4的时候,兼容了 字符串/数字/null

总结

jsoniter 包提供了比较完善的定制能力,通过例子可以感受一下扩展性。后续大家可以根据业务需求发掘更多的能力