功能说明
JSON Pointer 用类似 /items/1/name 的路径定位 JSON 文档中的节点,常用于 JSON Patch、配置覆盖和调试工具。这段代码实现了 RFC 6901 的解析与求值:支持对象键、数组下标、空指针(指向整个文档),以及 ~0、~1 两种转义。
实现会严格拒绝不以 / 开头的指针、非法转义、缺失键、越界下标和向标量继续下钻,避免返回模糊的零值掩盖数据问题。
关键代码
escapeToken 按规范先替换 ~ 为 ~0,再替换 / 为 ~1;顺序不能颠倒,否则会把新生成的 ~0 再次改写。decodeToken 逐字节扫描,遇到 ~ 时只接受 0 和 1 两种后续字符。
parsePointer 用 / 切分后逐段解码;根指针直接返回空 token 列表。Resolve 根据当前节点的类型分支:对象按键取值,数组用 strconv.Atoi 转换下标并检查边界,其他类型立即报错。
使用注意
解码后的 JSON 数字都是 float64,大整数可能丢失精度;需要精确数值时应使用 json.Decoder.UseNumber。数组下标接受 01 这类带前导零的写法,这是相对规范的简化。
- 在 JSON Patch 中表示“数组末尾追加”,但作为读取指针是非法的,代码会明确拒绝。指针中的键名必须已经转义,用户输入不能直接拼接。空字符串指针返回整个文档,调用方应确认这是期望行为。
验证方式
示例从 /items/1/name 取到 second,从 /a~1b/m~0n 取到 42,证明数组下标和转义键都能正确处理。escapeToken("a/b~c") 输出 a~1b~0c。越界下标 /items/9 和非法转义 /bad~2escape 都会返回错误并触发断言。
代码示例
package main
import (
"encoding/json"
"fmt"
"strconv"
"strings"
)
func escapeToken(token string) string {
token = strings.ReplaceAll(token, "~", "~0")
return strings.ReplaceAll(token, "/", "~1")
}
func decodeToken(token string) (string, error) {
var builder strings.Builder
for index := 0; index < len(token); index++ {
if token[index] != '~' {
builder.WriteByte(token[index])
continue
}
if index+1 >= len(token) {
return "", fmt.Errorf("dangling ~ in token %q", token)
}
switch token[index+1] {
case '0':
builder.WriteByte('~')
case '1':
builder.WriteByte('/')
default:
return "", fmt.Errorf("invalid escape ~%c in token %q", token[index+1], token)
}
index++
}
return builder.String(), nil
}
func parsePointer(pointer string) ([]string, error) {
if pointer == "" {
return nil, nil
}
if !strings.HasPrefix(pointer, "/") {
return nil, fmt.Errorf("pointer must start with /: %q", pointer)
}
rawParts := strings.Split(pointer[1:], "/")
tokens := make([]string, 0, len(rawParts))
for _, part := range rawParts {
token, err := decodeToken(part)
if err != nil {
return nil, err
}
tokens = append(tokens, token)
}
return tokens, nil
}
func Resolve(document any, pointer string) (any, error) {
tokens, err := parsePointer(pointer)
if err != nil {
return nil, err
}
current := document
for _, token := range tokens {
switch node := current.(type) {
case map[string]any:
value, ok := node[token]
if !ok {
return nil, fmt.Errorf("missing key %q", token)
}
current = value
case []any:
if token == "-" {
return nil, fmt.Errorf("'-' is only valid when appending")
}
index, err := strconv.Atoi(token)
if err != nil || index < 0 || index >= len(node) {
return nil, fmt.Errorf("invalid array index %q", token)
}
current = node[index]
default:
return nil, fmt.Errorf("cannot descend into %T at %q", current, token)
}
}
return current, nil
}
func main() {
payload := `{"items":[{"name":"first"},{"name":"second"}],"a/b":{"m~n":42}}`
document := map[string]any{}
if err := json.Unmarshal([]byte(payload), &document); err != nil {
panic(err)
}
name, err := Resolve(document, "/items/1/name")
if err != nil || name != "second" {
panic("failed to resolve array element")
}
value, err := Resolve(document, "/a~1b/m~0n")
if err != nil || value != float64(42) {
panic("failed to resolve escaped keys")
}
fmt.Println(name, value, escapeToken("a/b~c"))
if _, err := Resolve(document, "/items/9"); err == nil {
panic("out-of-range index should fail")
}
if _, err := Resolve(document, "/bad~2escape"); err == nil {
panic("invalid escape should fail")
}
}