使用go反射校验json是否符合格式
背景
工作中碰到需要对sbom格式进行校验的情况,sbom官方库没有提供对应的函数操作,所以需要自己根据官方提供的结构体解析对应的json文件,json库提供的反序列化函数对于标准的json格式数据无能为力所以需要自己进行解析。
go反射
反射:Go语言提供了一种机制在运行时更新和检查变量的值、调用变量的方法和变量支持的内在操作,但是在编译时并不知道这些变量的具体类型,这种机制被称为反射。反射也可以让我们将类型本身作为第一类的值类型处理。
具体实现思路
-
确认json格式所能支持的数据结构
json支持格式包括:string/int/float/array/struct -
对应的数据结构设置对应解析
go反射的基础类型包括以下几种
type Kind uint const ( Invalid Kind = iota Bool Int Int8 Int16 Int32 Int64 Uint Uint8 Uint16 Uint32 Uint64 Uintptr Float32 Float64 Complex64 Complex128 Array Chan Func Interface Map Pointer Slice String Struct UnsafePointer )
go reflect库中包含了几种特殊处理
因为需要验证所有json中的数据,例如array中每一个数据,struct中每一个对象,例如如下数据我们需要对tmp数据为[]int{}类型进行校验
{ "tmp": [1,1,"asd"], }
很明显asd不符合我们的要求,但如果reflect.TypeOf()我们所能得到的结果为slice,并不能校验我们的需求,所以我们需要用到reflect.Type.Elem()获取对应的元素。
同样,我们设计结构体时还可能存在指针嵌套结构体的情况,此时也同样需要通过reflect.Type.Elem()获取对应的元素,具体可以查看reflect.Type.Elem()源码,如下func (t *rtype) Elem() Type { switch t.Kind() { case Array: tt := (*arrayType)(unsafe.Pointer(t)) return toType(tt.elem) case Chan: tt := (*chanType)(unsafe.Pointer(t)) return toType(tt.elem) case Map: tt := (*mapType)(unsafe.Pointer(t)) return toType(tt.elem) case Pointer: tt := (*ptrType)(unsafe.Pointer(t)) return toType(tt.elem) case Slice: tt := (*sliceType)(unsafe.Pointer(t)) return toType(tt.elem) } panic("reflect: Elem of invalid type " + t.String()) }
-
方法设计
首先因为不知道对应的json数据会存在多少层,所以使用递归的方式,对应的退出条件为- 递归至基础类型时判断且退出递归。
- 碰到struct类型时,将每个struct tag保存至map中,作为后续层级校验的标准,如果不包含在map中,则认为不符合所定义的结构体类型,退出递归。
- 碰到指针类型,则获取对应的值,value不变继续递归校验。
- slice/Array类型则遍历每一个元素,将元素和类型进行下一轮递归。
-
结合成为最终的解析函数
func VerifyJsonStruct(nextType reflect.Type, value any) bool { tmpType := nextType switch tmpKind := tmpType.Kind(); tmpKind { case reflect.Array, reflect.Slice: // 对应array的json类型,遍历元素 t := tmpType.Elem() if tmpV, ok := value.([]any); !ok { return false } else { for _, v := range tmpV { if !VerifyJson(t, v) { return false } } } case reflect.Pointer: t := tmpType.Elem() return VerifyJsonStruct(t, value) case reflect.Struct: numFieldMap := make(map[string]reflect.Type) for i := 0; i < tmpType.NumField(); i++ { filed := tmpType.Field(i) // 获取 struct tag,并存储至map中,作为传递条件,此时map中存储的为下一层数据结构 numFieldMap[splitJson(filed.Tag.Get("json"))] = filed.Type } if nextValue, ok := value.(map[string]any); !ok { return false } else { for k, v := range nextValue { // 判断后续层级中tag是否被map包含 if reType, ok := numFieldMap[k]; ok { if !VerifyJsonStruct(reType, v) { return false } } else { return false } } } default: vType := reflect.TypeOf(value).Kind() if tmpKind == vType { return true } else { return false } } return true }
总结
已实现:
- 当前代码已经能够检查数据类型错误,多余数据。
不足:
- 需要对所有结构体添加tag否则会存在问题
- 不能对设置必填的tag进行校验(实现不难,因为不同tag设置效果不同,根据自己需要情况进行定制)
后记
写完的时候才发现这个需求类似gorm的tag读取,所以如果本文如果不清楚可以去查看gorm框架的源码The fantastic ORM library for Golang, aims to be developer friendly.