golang JSON Schema验证插件库jiojoi的使用

Golang JSON Schema验证插件库jio的使用

jio是一个简单高效的JSON Schema验证库,可以帮助你在Golang中进行JSON数据验证。

为什么使用jio?

在Golang中进行参数验证是一个常见问题。在结构体上定义标签不容易扩展规则,手写验证代码会使逻辑代码变得冗长,结构体字段的初始零值也会干扰验证。

jio尝试在反序列化之前验证原始JSON数据来避免这些问题。将验证规则定义为Schema易于阅读和扩展(灵感来自Hapi.js的joi库)。

如何使用?

验证JSON字符串

package main

import (
    "log"

    "github.com/faceair/jio"
)

func main() {
    data := []byte(`{
        "debug": "on",
        "window": {
            "title": "Sample Widget",
            "size": [500, 500]
        }
    }`)
    _, err := jio.ValidateJSON(&data, jio.Object().Keys(jio.K{
        "debug": jio.Bool().Truthy("on").Required(),
        "window": jio.Object().Keys(jio.K{
            "title": jio.String().Min(3).Max(18),
            "size":  jio.Array().Items(jio.Number().Integer()).Length(2).Required(),
        }).Without("name", "title").Required(),
    }))
    if err != nil {
        panic(err)
    }
    log.Printf("%s", data) // {"debug":true,"window":{"size":[500,500],"title":"Sample Widget"}}
}

上面的schema定义了以下约束:

  • debug
    • 不能为空,验证结束时必须是布尔值
    • 允许使用"on"字符串代替true
  • window
    • 不能为空,必须是对象
    • 不允许同时存在"name"和"title"
    • 包含以下元素:
      • title
        • 字符串,可以为空
        • 不为空时长度在3到18之间
      • size
        • 数组,不能为空
        • 包含两个整数类型的子元素

使用中间件验证请求体

以chi为例,其他框架类似:

package main

import (
    "io/ioutil"
    "net/http"

    "github.com/faceair/jio"
    "github.com/go-chi/chi"
)

func main() {
    r := chi.NewRouter()
    r.Route("/people", func(r chi.Router) {
        r.With(jio.ValidateBody(jio.Object().Keys(jio.K{
            "name":  jio.String().Min(3).Max(10).Required(),
            "age":   jio.Number().Integer().Min(0).Max(100).Required(),
            "phone": jio.String().Regex(`^1[34578]\d{9}$`).Required(),
        }), jio.DefaultErrorHandler)).Post("/", func(w http.ResponseWriter, r *http.Request) {
            body, err := ioutil.ReadAll(r.Body)
            if err != nil {
                panic(err)
            }
            w.Header().Set("Content-Type", "application/json; charset=utf-8")
            w.WriteHeader(http.StatusOK)
            w.Write(body)
        })
    })
    http.ListenAndServe(":8080", r)
}

jio.ValidateBody的第二个参数在验证失败时被调用用于错误处理。

使用中间件验证查询参数

package main

import (
    "encoding/json"
    "net/http"

    "github.com/faceair/jio"
    "github.com/go-chi/chi"
)

func main() {
    r := chi.NewRouter()
    r.Route("/people", func(r chi.Router) {
        r.With(jio.ValidateQuery(jio.Object().Keys(jio.K{
            "keyword":  jio.String(),
            "is_adult": jio.Bool().Truthy("true", "yes").Falsy("false", "no"),
            "starts_with": jio.Number().ParseString().Integer(),
        }), jio.DefaultErrorHandler)).Get("/", func(w http.ResponseWriter, r *http.Request) {
            query := r.Context().Value(jio.ContextKeyQuery).(map[string]interface{})
            body, err := json.Marshal(query)
            if err != nil {
                panic(err)
            }
            w.Header().Set("Content-Type", "application/json; charset=utf-8")
            w.WriteHeader(http.StatusOK)
            w.Write(body)
        })
    })
    http.ListenAndServe(":8080", r)
}

注意查询参数的原始值是字符串,你可能需要先转换值类型(例如jio.Number().ParseString()jio.Bool().Truthy(values))。

高级用法

工作流程

每个Schema由一系列规则组成,例如:

jio.String().Min(5).Max(10).Alphanum().Lowercase()

在这个例子中,String Schema有4个规则,分别是Min(5) Max(10) Alphanum() Lowercase(),将按顺序验证Min(5) Max(10) Alphanum() Lowercase()。如果一个规则验证失败,Schema的验证将停止并抛出错误。

为了提高代码可读性,这三个内置规则会先验证:

  • Required()
  • Optional()
  • Default(value)

例如:

jio.String().Min(5).Max(10).Alphanum().Lowercase().Required()

实际的验证顺序将是Required() Min(5) Max(10) Alphanum() Lowercase()

验证完所有规则后,最后我们检查数据的基本类型是否是Schema的类型。如果不是,Schema将抛出错误。

验证器上下文

工作流中的数据传递依赖于上下文,结构如下:

type Context struct {
    Value interface{} // 原始数据,你也可以重新赋值来改变结果
}
func (ctx *Context) Ref(refPath string) (value interface{}, ok bool) { // 引用其他字段数据
}
func (ctx *Context) Abort(err error) { // 终止验证并抛出错误
    ...
}
func (ctx *Context) Skip() { // 跳过后续规则
    ...
}

让我们尝试自定义一个验证规则。使用Transform方法添加规则:

jio.String().Transform(func(ctx *jio.Context) {
    If ctx.Value != "faceair" {
        ctx.Abort(errors.New("you are not faceair"))
    }
})

我们添加的自定义规则意味着当原始数据不等于"faceair"时抛出"you are not faceair"错误。

事实上,内置验证规则的工作方式类似。例如,Optional()的核心代码是:

If ctx.Value == nil {
    ctx.Skip()
}

你也可以重新赋值ctx.Value来改变原始数据。例如,内置的Lowercase()将原始字符串转换为小写。核心代码是:

ctx.Value = strings.ToLower(ctx.Value)

引用和优先级

在大多数情况下,规则只使用当前字段的数据,但有时需要与其他字段配合工作。例如:

{
    "type": "ip", // 枚举值,`ip`或`domain`
    "value": "8.8.8.8"
}

这个value的验证规则由type的值决定,可以写成:

jio.Object().Keys(jio.K{
        "type": jio.String().Valid("ip", "domain").SetPriority(1).Default("ip"),
        "value": jio.String().
            When("type", "ip", jio.String().Regex(`^\d+\.\d+\.\d+\.\d+$`)).
            When("type", "domain", jio.String().Regex(`^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0 -9]\.[a-zA-Z]{2,}$`)).Required(),
})

When函数可以引用其他字段数据,如果成功,则将新的验证规则应用于当前数据。

此外,你可能注意到type的规则中有一个SetPriority方法。如果输入数据是:

{
    "value": "8.8.8.8"
}

当未设置优先级时,value的验证规则可能会先执行。此时引用的type值将为null,验证将失败。 因为有相互引用的验证规则,可能存在验证顺序要求。当我们希望同一个Object下的某个字段先验证时,可以将其设置为较大的优先级值(默认值0)。

如果你想在自定义规则中引用其他字段的数据,可以使用上下文上的Ref方法。如果引用的数据是嵌套对象,引用字段的路径需要用.连接。例如,如果你想引用people对象下的name,那么引用路径是people.name

{
    "type": "people",
    "people": {
        "name": "faceair"
    }
}

更多关于golang JSON Schema验证插件库jiojoi的使用的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于golang JSON Schema验证插件库jiojoi的使用的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


jiojoi - Golang JSON Schema 验证库

jiojoi 是一个轻量级的 Golang JSON Schema 验证库,它提供了简单易用的 API 来验证 JSON 数据是否符合预定义的 schema 规范。

安装

go get github.com/faceair/jiojoi

基本用法

1. 定义 Schema

首先需要定义一个 JSON Schema 来描述你的数据结构:

package main

import (
	"fmt"
	"github.com/faceair/jiojoi"
)

func main() {
	schema := `{
		"type": "object",
		"properties": {
			"name": {
				"type": "string",
				"minLength": 3,
				"maxLength": 20
			},
			"age": {
				"type": "integer",
				"minimum": 0,
				"maximum": 150
			},
			"email": {
				"type": "string",
				"format": "email"
			}
		},
		"required": ["name", "age"]
	}`

2. 验证 JSON 数据

	// 要验证的 JSON 数据
	data := `{
		"name": "John Doe",
		"age": 30,
		"email": "john@example.com"
	}`

	// 创建验证器
	validator, err := jiojoi.NewValidatorFromString(schema)
	if err != nil {
		fmt.Println("Schema 解析错误:", err)
		return
	}

	// 验证数据
	result, err := validator.ValidateBytes([]byte(data))
	if err != nil {
		fmt.Println("验证错误:", err)
		return
	}

	if result.Valid() {
		fmt.Println("数据验证通过!")
	} else {
		fmt.Println("数据验证失败:")
		for _, err := range result.Errors() {
			fmt.Printf("- %s\n", err)
		}
	}
}

高级功能

自定义验证规则

// 自定义验证函数
func validateEvenNumber(value interface{}) error {
	num, ok := value.(float64)
	if !ok {
		return fmt.Errorf("必须是数字")
	}
	if int(num)%2 != 0 {
		return fmt.Errorf("必须是偶数")
	}
	return nil
}

func main() {
	schema := `{
		"type": "object",
		"properties": {
			"even_number": {
				"type": "number",
				"custom": "even"
			}
		}
	}`

	validator, err := jiojoi.NewValidatorFromString(schema)
	if err != nil {
		panic(err)
	}

	// 注册自定义验证器
	validator.RegisterCustomValidator("even", validateEvenNumber)

	data := `{"even_number": 3}`
	result, _ := validator.ValidateBytes([]byte(data))
	if !result.Valid() {
		fmt.Println(result.Errors()[0]) // 输出: even_number: 必须是偶数
	}
}

条件验证

schema := `{
	"type": "object",
	"properties": {
		"country": {
			"type": "string",
			"enum": ["US", "CA", "UK"]
		},
		"zip_code": {
			"type": "string"
		}
	},
	"if": {
		"properties": {
			"country": { "const": "US" }
		}
	},
	"then": {
		"properties": {
			"zip_code": {
				"pattern": "^[0-9]{5}(-[0-9]{4})?$"
			}
		}
	}
}`

性能优化技巧

  1. 重用验证器实例:验证器实例可以安全地并发使用,应该重用而不是每次验证都创建新实例

  2. 预编译 Schema:对于频繁使用的 Schema,可以预编译并缓存验证器

var userValidator *jiojoi.Validator

func init() {
	var err error
	userValidator, err = jiojoi.NewValidatorFromFile("user_schema.json")
	if err != nil {
		panic(err)
	}
}

func ValidateUser(data []byte) error {
	result, err := userValidator.ValidateBytes(data)
	if err != nil {
		return err
	}
	if !result.Valid() {
		return fmt.Errorf("invalid user data: %v", result.Errors())
	}
	return nil
}

与其他库的比较

  1. gojsonschema:功能更全面但更重量级
  2. santhosh-tekuri/jsonschema:性能较好但 API 较复杂
  3. jiojoi:轻量级,API 简单,适合大多数基本验证需求

注意事项

  1. 确保 Schema 是有效的 JSON
  2. 错误信息可能不够详细,需要根据业务需求进一步处理
  3. 对于复杂的数据结构,考虑将 Schema 拆分为多个部分

jiojoi 是一个简单实用的 JSON Schema 验证工具,适合大多数 Golang 项目中的 JSON 数据验证需求。

回到顶部