使用go反射校验json是否符合格式

使用go反射校验json是否符合格式

背景

工作中碰到需要对sbom格式进行校验的情况,sbom官方库没有提供对应的函数操作,所以需要自己根据官方提供的结构体解析对应的json文件,json库提供的反序列化函数对于标准的json格式数据无能为力所以需要自己进行解析。

go反射

反射:Go语言提供了一种机制在运行时更新和检查变量的值、调用变量的方法和变量支持的内在操作,但是在编译时并不知道这些变量的具体类型,这种机制被称为反射。反射也可以让我们将类型本身作为第一类的值类型处理。

具体实现思路

  1. 确认json格式所能支持的数据结构
    json支持格式包括:string/int/float/array/struct

  2. 对应的数据结构设置对应解析

    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())
    }
    
  3. 方法设计
    首先因为不知道对应的json数据会存在多少层,所以使用递归的方式,对应的退出条件为

    • 递归至基础类型时判断且退出递归。
    • 碰到struct类型时,将每个struct tag保存至map中,作为后续层级校验的标准,如果不包含在map中,则认为不符合所定义的结构体类型,退出递归。
    • 碰到指针类型,则获取对应的值,value不变继续递归校验。
    • slice/Array类型则遍历每一个元素,将元素和类型进行下一轮递归。
  4. 结合成为最终的解析函数

    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
    }
    

总结

已实现:

  1. 当前代码已经能够检查数据类型错误,多余数据。

不足:

  1. 需要对所有结构体添加tag否则会存在问题
  2. 不能对设置必填的tag进行校验(实现不难,因为不同tag设置效果不同,根据自己需要情况进行定制)

后记

写完的时候才发现这个需求类似gorm的tag读取,所以如果本文如果不清楚可以去查看gorm框架的源码The fantastic ORM library for Golang, aims to be developer friendly.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值