很多开发者在终端里处理 JSON 时都装过 jq,但 jq 的查询语法并不直观。今天我们用 OpenCode 4 分钟写一个更轻量的命令行 JSON 工具——支持格式化美化、路径查询、语法校验三种模式,最终代码约 120 行,零外部依赖。
技术栈:Go 1.18+,纯标准库实现。
最终效果:
# 格式化 JSON cat data.json | jsonx fmt # 按路径取值 cat data.json | jsonx get users.0.name # 校验 JSON 语法 cat data.json | jsonx validate
npm i -g @opencode/cli 或前往 opencode.ai 获取)我对 OpenCode 说:
> 在当前目录初始化一个 Go 模块,module 名为 jsonx
OpenCode 执行了 go mod init jsonx,go.mod 文件自动生成。
我对 OpenCode 说:
> 创建 main.go,实现一个命令行工具 jsonx,支持三个子命令:fmt(格式化 JSON)、get(按点号分隔路径查询 JSON 值)、validate(校验 JSON 语法)。输入来自 stdin。用 flag 包解析子命令。
这个提示词描述了完整的程序结构。OpenCode 生成了以下骨架:
package main
import (
"encoding/json"
"flag"
"fmt"
"io"
"os"
"strings"
)
func main() {
flag.Parse()
args := flag.Args()
if len(args) == 0 {
fmt.Println("Usage: jsonx <fmt|get|validate> [path]")
return
}
data, _ := io.ReadAll(os.Stdin)
switch args[0] {
case "fmt":
format(data)
case "get":
if len(args) < 2 {
fmt.Println("get requires a path")
return
}
queryGet(data, args[1])
case "validate":
validate(data)
}
}
关键点:我一次性描述了三个子命令的职责,OpenCode 准确理解了"子命令 + 参数"的 CLI 模式,并生成了对应的 switch 分支。
我对 OpenCode 说:
> 实现 format 函数:将 JSON 字节解析后重新用 json.MarshalIndent 输出,缩进两个空格。如果 JSON 无效则输出错误信息。
遇到 JSON 解析失败时,OpenCode 会自动意识到需要输出可读的错误信息,而不是 panic。生成结果:
func format(data []byte) {
var v interface{}
if err := json.Unmarshal(data, &v); err != nil {
fmt.Fprintln(os.Stderr, "invalid JSON:", err)
os.Exit(1)
}
out, _ := json.MarshalIndent(v, "", " ")
fmt.Println(string(out))
}
我对 OpenCode 说:
> 实现 queryGet 函数:用 strings.Split(path, ".") 分割路径,逐层访问 JSON。支持数组索引(把 "0"、"1" 等纯数字路径段转换成 int)。如果路径不存在则返回 "null"。
这是最复杂的部分。OpenCode 需要理解 JSON 的嵌套结构遍历逻辑。它生成了:
func queryGet(data []byte, path string) {
var v interface{}
json.Unmarshal(data, &v)
parts := strings.Split(path, ".")
for _, part := range parts {
switch val := v.(type) {
case map[string]interface{}:
var ok bool
v, ok = val[part]
if !ok {
fmt.Println("null")
return
}
case []interface{}:
idx := 0
fmt.Sscanf(part, "%d", &idx)
if idx < 0 || idx >= len(val) {
fmt.Println("null")
return
}
v = val[idx]
default:
fmt.Println("null")
return
}
}
out, _ := json.MarshalIndent(v, "", " ")
fmt.Println(string(out))
}
这里展示了一个关键能力:我只说了"支持数组索引",OpenCode 就自动实现了用 fmt.Sscanf 把字符串转成 int,并在 []interface{} 分支中按索引取值。边界处理(index 越界返回 null)也是它自己加的。
我对 OpenCode 说:
> 实现 validate 函数:校验 JSON 是否合法,合法输出 "valid JSON",不合法输出具体错误信息。
OpenCode 生成了一个简洁的校验函数:
func validate(data []byte) {
var v interface{}
if err := json.Unmarshal(data, &v); err != nil {
fmt.Fprintln(os.Stderr, "invalid JSON:", err)
os.Exit(1)
}
fmt.Println("valid JSON")
}
我对 OpenCode 说:
> 编译并测试:用 echo 构造一段 JSON 分别测试 fmt、get、validate 三个命令
OpenCode 依次运行:
go build -o jsonx.exe .
echo '{"users":[{"name":"Alice"},{"name":"Bob"}]}' | ./jsonx.exe fmt
echo '{"users":[{"name":"Alice"},{"name":"Bob"}]}' | ./jsonx.exe get users.1.name
echo '{"users":[{"name":"Alice"},{"name":"Bob"}]}' | ./jsonx.exe validate
全部输出正确。
package main
import (
"encoding/json"
"flag"
"fmt"
"io"
"os"
"strings"
)
func main() {
flag.Parse()
args := flag.Args()
if len(args) == 0 {
fmt.Fprintln(os.Stderr, "Usage: jsonx <fmt|get|validate> [path]")
fmt.Fprintln(os.Stderr, " fmt - pretty-print JSON")
fmt.Fprintln(os.Stderr, " get PATH - extract value by dot-path, e.g. users.0.name")
fmt.Fprintln(os.Stderr, " validate - check if input is valid JSON")
os.Exit(1)
}
data, err := io.ReadAll(os.Stdin)
if err != nil {
fmt.Fprintln(os.Stderr, "read stdin:", err)
os.Exit(1)
}
switch args[0] {
case "fmt":
format(data)
case "get":
if len(args) < 2 {
fmt.Fprintln(os.Stderr, "get requires a path argument")
os.Exit(1)
}
queryGet(data, args[1])
case "validate":
validate(data)
default:
fmt.Fprintf(os.Stderr, "unknown command: %s\n", args[0])
os.Exit(1)
}
}
func format(data []byte) {
var v any
if err := json.Unmarshal(data, &v); err != nil {
fmt.Fprintln(os.Stderr, "invalid JSON:", err)
os.Exit(1)
}
out, _ := json.MarshalIndent(v, "", " ")
fmt.Println(string(out))
}
func queryGet(data []byte, path string) {
var v any
if err := json.Unmarshal(data, &v); err != nil {
fmt.Fprintln(os.Stderr, "invalid JSON:", err)
os.Exit(1)
}
parts := strings.Split(path, ".")
for _, part := range parts {
switch val := v.(type) {
case map[string]any:
var ok bool
v, ok = val[part]
if !ok {
fmt.Println("null")
return
}
case []any:
idx := 0
fmt.Sscanf(part, "%d", &idx)
if idx < 0 || idx >= len(val) {
fmt.Println("null")
return
}
v = val[idx]
default:
fmt.Println("null")
return
}
}
out, _ := json.MarshalIndent(v, "", " ")
fmt.Println(string(out))
}
func validate(data []byte) {
var v any
if err := json.Unmarshal(data, &v); err != nil {
fmt.Fprintln(os.Stderr, "invalid JSON:", err)
os.Exit(1)
}
fmt.Println("valid JSON")
}
这个约 120 行的工具从零到可运行,与 OpenCode 对话不超过 10 个来回,耗时约 4 分钟。真正有价值的地方在于:
json.Unmarshal 的类型断言逻辑、fmt.Sscanf 的用法 OpenCode 都直接生成。如果你每天和 API、配置文件、日志里的 JSON 打交道,花 4 分钟给自己造个趁手工具,比折腾 jq 语法划算得多。