登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

Go jsonschema 接收的 Go 数值类型为什么影响 JSON 校验

来源:17golang原创

时间:2026-10-30 23:11:47 303浏览 收藏

Go 里用 github.com/santhosh-tekuri/jsonschema/v5 校验 JSON 数值时,Schema 写对只是第一步。真正容易被忽略的是:JSON 进入 interface{} 后到底变成了 float64、json.Number,还是某一种整数类型。这个差异会影响 integer、number、minimum、maximum 等约束的解释,也会影响大整数是否还能保持原值。

官方地址:https://pkg.go.dev/github.com/santhosh-tekuri/jsonschema/v5

实用结论:外部 JSON 默认用 json.Decoder.UseNumber() 解码,再把数值策略固定在校验入口;不要让不同接口一会儿传 float64,一会儿传手工构造的 int64,最后用同一套错误信息解释所有结果。Schema 的 integer 表示没有小数部分的 JSON 数值,Go 的 int 只是其中一种承载方式。

先把数值校验放回完整链路

在一个高并发配置或请求校验服务里,数字通常要经过三层:请求解码层把字节变成 Go 值,Schema 层判断类型和值域,业务层再把通过校验的值映射到具体结构体。问题往往不在某个关键字,而在三层对“数字”的含义没有对齐。

Go jsonschema 数值输入、解码类型与 Schema 约束之间的静态关系说明图
图1:数值校验关系说明图,展示 JSON 输入、Go 解码类型、Schema 约束和值映射的静态边界;这不是运行截图。

JSON Schema 自己只有 number 和 integer 这两个数值类型。它并不要求你必须用 Go 的 int 表示 integer,也不把 float64 自动等同为“只能通过 number”。校验器收到的是 Go 值,库会根据数值类型和数值内容判断它能否满足 Schema。

因此排查时应按这个顺序问三个问题:

  • JSON 中的字段是整数文本,还是带小数点的数字文本?
  • 解码器把它保存成了 float64 还是 json.Number?
  • Schema 要求的是任意数值、整数语义,还是额外的范围和倍数约束?

原方案的瓶颈:默认 Unmarshal 把数字都放进 float64

最短的写法是把 JSON 解码到 any:

var document any
if err := json.Unmarshal(payload, &document); err != nil {
    // 解码失败先返回,不能让半成品进入 Schema 校验。
    return err
}

当目标是 interface{} 或 any 时,标准库默认把 JSON 数字解码为 float64。对普通金额比例、温度或分页参数,这通常足够;但它会带来两个工程边界:

  1. 数字原本是否写成整数,在 Go 值里不再由静态类型表达。后续看到的可能只是 float64(3)。
  2. 超过浮点安全精度的整数可能无法保持每一位原值。即使 Schema 的 maximum 很宽,业务层也可能已经拿不到原始整数。

这并不意味着默认解码一定会让 integer 失败。对值为 3 的 float64,校验器可以按数值内容判断它没有小数部分;真正的问题是,浮点承载方式把精度和后续转换责任推迟了。小数 3.14 不应通过 integer,而整数值的 3.0 仍然要按 JSON Schema 的数值语义来理解,不能只看 JSON 文本表面。

新入口:用 UseNumber 保留 JSON 数字的文本语义

如果接口需要处理订单号、外部系统计数器或可能超过浮点精度的整数,建议在解码阶段打开 UseNumber:

func decodeDocument(payload []byte) (any, error) {
    // UseNumber 让 JSON 数字先保留为 json.Number,避免立即变成 float64。
    decoder := json.NewDecoder(bytes.NewReader(payload))
    decoder.UseNumber()

    var document any
    if err := decoder.Decode(&document); err != nil {
        // 输入不是合法 JSON 时,不继续调用 Schema.Validate。
        return nil, err
    }
    return document, nil
}

json.Number 是字符串形式的数字包装,不是自动完成业务类型转换的魔法。它的价值在于把“这串数字是什么”留到更合适的边界决定:校验器可以先判断 JSON Schema,业务层再根据字段语义调用 Int64() 或 Float64(),转换失败也能明确返回。

这套做法对高吞吐入口的好处是边界一致:所有外部 JSON 都先经过同样的解码器,Schema 看到的是同一种数字表示;代价是业务层不能再假设拿到的一定是 float64,需要在映射阶段显式处理 json.Number。

Go jsonschema 中 float64、json.Number、int64 与 integer 和 number 约束的静态关系图
图2:数值类型关系结构图,展示 float64、json.Number、整数转换与 number/integer 语义之间的静态关系;这不是运行结果截图。

把 CompileString 和 Validate 放在同一个校验入口

下面的示例只讨论“解码类型如何进入校验器”,不把 Schema 编译、请求解码和业务落库混在一起。Schema 使用 integer、minimum 和 maximum,可以同时观察整数语义与范围语义:

func validatePayload(payload []byte) error {
    // Schema 明确要求 count 是整数,并限制在业务允许的范围内。
    schemaText := `{
      "type": "object",
      "required": ["count"],
      "properties": {
        "count": {
          "type": "integer",
          "minimum": 0,
          "maximum": 1000000
        }
      },
      "additionalProperties": false
    }`

    // CompileString 负责把 JSON Schema 编译成可复用的 Schema。
    schema, err := jsonschema.CompileString("count.schema.json", schemaText)
    if err != nil {
        // Schema 本身有误时,应该在启动或配置加载阶段修复。
        return err
    }

    // 统一使用 json.Number 解码,避免大整数先被 float64 舍入。
    decoder := json.NewDecoder(bytes.NewReader(payload))
    decoder.UseNumber()

    var document any
    if err := decoder.Decode(&document); err != nil {
        // 语法错误与 Schema 不匹配是两类不同问题。
        return err
    }

    // Validate 返回的 ValidationError 可继续输出层级和字段位置。
    if err := schema.Validate(document); err != nil {
        return err
    }
    return nil
}

生产代码通常会在启动时编译一次 Schema,而不是每个请求都编译;这里只把代码压缩在一个示例里,方便看清数据边界。库文档说明 Schema.Validate 会返回包含层级上下文的 ValidationError,因此业务日志可以保留字段位置与关键字路径,而不是只记录“校验失败”。

关键取舍:number、integer 和 Go 类型不要互相替代

场景Schema 选择解码建议注意点
计数、页码、数量integerUseNumber,通过后转整数再设置 minimum,避免负数绕过业务规则
比例、测量值、带小数金额numberUseNumber 或明确的浮点映射不要用 integer 代替小数约束
外部大整数标识按协议约束整数范围优先保留 json.Number校验通过后再决定转 int64、uint64 或字符串
内部手工构造测试值与外部 JSON 使用同一 Schema显式选择整数或浮点类型不要让测试数据类型与真实请求类型完全不同

这里的“同一类型”不是要求所有字段都转换成 int64。更重要的是:同一个字段在不同入口不要随意切换语义。例如,配置文件使用 json.Number,而内部补偿请求手工塞入 float64,两条路径虽然可能都通过,却会让范围边界和错误排查变得不一致。

上线后的排查信号与后续改进

当某个数字字段在生产环境突然出现“明明是整数却不通过”的反馈,可以优先按输入类型排查,而不是立刻改宽 Schema:

  1. 记录字段路径、Schema 关键字路径和 Go 值的类型,不要只记录格式化后的数字。
  2. 区分解码错误、类型错误和范围错误;三者的修复位置分别是 JSON 输入、Schema 或业务映射。
  3. 检查是否有一条路径使用默认 json.Unmarshal,另一条路径使用了 UseNumber。
  4. 对大整数明确协议:它是数值、字符串化 ID,还是仅用于展示的文本。

如果校验服务已经承担了多个系统的入口,下一步可以把 Schema 编译结果缓存起来,把统一解码器和错误输出封装成一个小包,再为“普通整数、带小数、超大整数、缺失字段”各保留一组输入样例。这样做的代价是增加一层公共代码,但能把数值策略从业务 handler 中抽离出来。

需要特别注意,json.Number 只解决“不要过早转成浮点数”,不替你决定金额精度、溢出策略和数据库字段类型。校验通过后仍要在业务层选择合适的表示,并处理 Int64() 或 Float64() 的转换错误。

常见问题

默认 json.Unmarshal 得到 float64,integer 一定会失败吗?

不一定。整数值的 float64 仍可能被按没有小数部分的 JSON 数值处理;真正危险的是精度丢失和后续转换假设,尤其是大整数。

Schema 写 integer,Go 结构体字段必须是 int 吗?

不必须。Schema 约束的是 JSON 数值语义,Go 字段类型是映射层的设计。可以先用 json.Number 做校验,再按字段边界转换成合适的整数类型。

为什么不直接把所有数字都改成字符串?

字符串能保留文本,但会改变 JSON Schema 的类型语义;如果协议确实规定它是字符串化 ID,就把 Schema 写成 string 并配合模式或长度约束,不要为了规避精度问题悄悄改变协议。

校验失败时应该打印什么?

至少保留字段位置、Schema 关键字路径和原始 Go 数值类型。jsonschema 的 ValidationError 支持更丰富的错误输出,适合在日志和接口响应之间做不同程度的裁剪。

小结

Go jsonschema 数值问题的核心不是“选 int 还是 float64”这么简单,而是让 JSON 解码、Schema 语义和业务转换拥有同一条边界。普通数据可以直接使用默认解码;只要涉及大整数、严格的整数语义或多来源输入,就应在入口使用 UseNumber,先完成 Schema 校验,再按字段语义完成安全转换。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>