px普贤教程PuXian
普贤教程 › 基础教程

JSON 处理

JSON 在 PuXian 里不是"序列化格式",而是字典与列表的直接映射。两个函数就够:json_stringify 与 json_parse_opt。

序列化

px
var user = {
    "id": 1024,
    "name": "晨曦",
    "tags": ["qa", "px"],
    "active": true,
    "score": 9.5,
    "note": null
}

print(json_stringify(user))
text
{"id":1024,"name":"晨曦","tags":["qa","px"],"active":true,"score":9.5,"note":null}

输出是紧凑格式(无空格、无换行),键顺序不保证与书写顺序一致。要好看的缩进格式,用 std.go_json_indent。

解析

px
var text = "{\"name\":\"晨曦\",\"tags\":[\"a\",\"b\"],\"n\":3}"
var r = json_parse_opt(text)
if r.is_err():
    print("解析失败:" + r.err())
else:
    var v = r.ok()
    print(v["name"])
    print(str(len(v["tags"])))
    print(str(v["n"]))
text
晨曦
2
3
永远用 json_parse_optjson_parse 遇到非法输入抛的是不可捕获的运行时错误(0.2.382 仍没有 try/catch),本意返回 400 的接口会变成 500。json_parse_opt 把失败变成 Err,能正常处理。
px
print(str(json_parse_opt("{oops").is_err()))
text
true

类型映射

JSONPuXian
对象 {...}dict
数组 [...]list
整数int
小数float
字符串string
true / falsebool
nullnull

顶层是数组时解析得到的就是 list:

px
var arr = json_parse_opt("[1,2,3]").ok()
print(type(arr))
print(str(arr[1]))
text
list
2

安全取值

外部 JSON 结构不可信。缺键会直接报 R1008,所以取值前逐层判断:

px
def pick(d, path, fallback):
    # path 形如 "meta.region"
    var cur = d
    for seg in split(path, "."):
        if type(cur) != "dict":
            return fallback
        if not cur.has(seg):
            return fallback
        cur = cur[seg]
    if cur == null:
        return fallback
    return cur

var resp = {"meta": {"region": "cn-north"}, "items": []}
print(str(pick(resp, "meta.region", "-")))
print(str(pick(resp, "meta.zone", "-")))
print(str(pick(resp, "missing.deep.key", "-")))
text
cn-north
-
-

构造响应

px
def ok_resp(data):
    return json_stringify({"ok": true, "data": data, "ts": now_ms()})

def err_resp(code, msg):
    return json_stringify({"ok": false, "error": msg, "code": code})

print(ok_resp({"user": "晨曦"}))
print(err_resp(400, "参数错误"))
text
{"ok":true,"data":{"user":"晨曦"},"ts":1793869xxxxx}
{"ok":false,"error":"参数错误","code":400}
约定优于技巧接口统一用 {"ok":bool,...} 外壳,前端只需要判一个字段。错误里带 code 与 message,日志里能直接 grep 出来。

数组遍历与转换

px
var rows = json_parse_opt("[{\"n\":1},{\"n\":2},{\"n\":3}]").ok()
var total = 0
for r in rows:
    if r.has("n"):
        total += r["n"]
print(str(total))
print(str(len(rows)))
text
6
3

写进文件与读回来

px
write_file("/tmp/px_data.json", json_stringify({"a": 1, "b": [1, 2]}))
var back = json_parse_opt(read_file("/tmp/px_data.json")).ok()
print(str(back["a"]))
print(str(len(back["b"])))
text
1
2

常见问题

问题原因与处理
解析中文乱码源不是 UTF-8;先转码再解析
数字精度丢失大整数以 float 存;需要精确值时用字符串承载
键顺序不对字典无序,需要顺序就自己排序后拼
嵌套太深取值易错用上面的 pick() 辅助函数逐层判空

与 Go 的差异

要让输出与 Go encoding/json 逐字节对拍,下面几处必须自己补,标准库不负责。

项GoPuXian 0.2.382
\b \f输出 \b \fjson_stringify_go 输出 \u0008 \u000c
非法 UTF-8原始 U+FFFD输出 \ufffd 转义
解析合法性严格json_parse_opt 明显更宽松(见下)
json_parse_opt 比 Go 宽松下面这些 Go 全判非法,px 都返回 Ok:尾随垃圾 {"a":1}x、前导零 01、{"a":+1}、未知转义 {"a":"\q"}。从 Go 迁过来的输入校验会静默放过非法报文。(未闭合字符串、尾随逗号 [1,] 会返回 Err,这两类与 Go 一致。)

要 Go json.Valid 同语义,用 std.go_json_scan(Go scanner 状态机的逐字节复刻)自己包一层:

px
import std.go_json_scan

def go_json_valid(src):
    var s = gjs_new()
    var n = len(src)
    var i = 0
    while i < n:
        gjs_step(s, src[i], i)
        i = i + 1
    return gjs_eof(s, n) == GJS_END

print(str(go_json_valid("{\"a\":1}")))     # true
print(str(go_json_valid("{\"a\":1}x")))    # false
print(str(go_json_valid("01")))            # false
text
true
false
false