nu-json 深度解析:Nushell 的人本 JSON(Hjson)解析与序列化库
本文基于 Nushell 仓库中的 nu-json crate 文档及其源码实现,系统讲解这个 Human JSON(Hjson)库的定位、依赖配置、解析与序列化的完整 API、错误报告机制,以及它如何通过 preserve_order 与 nu-protocol 特性融入 Nushell 的数据管线。读完本文,你可以直接在自己的 Rust 项目中引入 nu-json 处理宽松的 JSON 配置,也能理解 Nushell 中 from json / to json 背后的底层引擎。
nu-json 是什么:serde-hjson 的官方分叉
crates/nu-json/README.md 开篇即声明:nu-json 是 Rust 生态中 serde-hjson crate 的分叉(fork),对原 crate 的所有改动记录在 CHANGELOG 中。它本质上是一个 Rust 库,用于解析与生成 Human JSON(Hjson),并构建在 Serde 这一高性能通用序列化框架之上,Rust 侧的 Hjson 实现则参考了 serde-rs 的 JSON 序列化库的设计。
crates/nu-json/Cargo.toml 中的元信息印证了这一定位:
description = "Fork of serde-hjson",作者为 Nushell 项目开发者;- 核心依赖为
serde、serde_json、num-traits,以及 Nushell 工作区内的nu-utils; nu-protocol是可选依赖(optional = true),只有启用对应 feature 时才参与编译——这正是它接入 Nushell 管线的关键开关。
从源码结构看,lib.rs 第一行用 #![doc = include_str!("../README.md")] 把 README 直接内嵌为 crate 的文档首页,也就是说这份 README 既是仓库文档,也是发布到 crates.io 后使用者看到的官方文档。
在 Nushell 的演进历史中,CHANGELOG 记录了它的起点:0.22.0(2020-11-22)版本标注 “Fork of serde-hjson”,该 crate 由此加入 Nu 项目工作区,此后与 Nushell 主版本同步发版(如 0.76.0 于 2023-02-21 发布)。
Hjson:对人类友好的宽松语法
README 的用法示例本身就是一份 Hjson 语法说明,其中用到了注释与无引号键值:
{
## specify rate in requests/second
rate: 1000
array:
[
foo
bar
]
}
对照源码可以看到这些宽松特性是如何实现的:
注释支持。 util.rs 中的 parse_whitespace 方法在跳过空白时同时处理三类注释:# 开头的行内注释(eat_line 直接吞掉整行)、// 双斜杠行注释,以及 /* ... */ 块注释(循环消费字符直到遇到 */ 结束标记)。这正是 Hjson 相对于严格 JSON 最显著的能力之一。
无引号字符串与键。 error.rs 定义的 ErrorCode 中有一个 JSON 解析器绝不会出现的变体 PunctuatorInQlString,其文档注释为 “Found a punctuator character when expecting a quoteless string”(在期望无引号字符串的位置发现了标点字符)。从源码结构看,这个专用错误码的存在说明反序列化器内置了完整的无引号字符串(quoteless string)词法逻辑,用于解析 rate: 1000 这类省略引号的键与值。
数字分级解析。 util.rs 中的 ParseNumber 把数字先缓冲为字节串,再按规则归类:无小数点与指数时优先解析为 i64(负数)或 u64(非负),否则回退为 f64。这与 value.rs 中 Value 枚举同时提供 I64、U64、F64 三个数字变体相呼应,保留了整数的精确性而不必一律转浮点。CHANGELOG 还记录了 0.66.0 版本 “Prevents panic when parsing JSON containing large number”——修复了大数字解析时的 panic,属于该数字解析路径上的健壮性补丁。
安装与依赖配置
README 给出的安装方式是通过 Cargo 引入(crate 发布在 crates.io):
[dependencies]
serde = "1"
nu-json = "0.76"
或者用命令行:
cargo add serde
cargo add nu-json
值得注意的特性(feature)配置在 Cargo.toml 中:
[features]
preserve_order = ["linked-hash-map", "linked-hash-map/serde_impl", "serde_json/preserve_order"]
default = ["preserve_order"]
preserve_order 是默认开启的特性,它引入 linked-hash-map 以保持键的书写顺序。若关闭该 feature,value.rs 会改用标准库的 BTreeMap 作为 Map 的实现,此时键会按字典序排列而非出现顺序:
/// Represents a key/value type.
#[cfg(not(feature = "preserve_order"))]
pub type Map<K, V> = BTreeMap<K, V>;
/// Represents a key/value type.
#[cfg(feature = "preserve_order")]
pub type Map<K, V> = LinkedHashMap<K, V>;
这一行为在 CHANGELOG 中有明确对应:0.28.0(2021-03-09)记录 “Preserve order when serializing/deserialize json by default”——即把“默认保序”确立为行为准则,对配置类数据(键的顺序往往有可读性意义)尤为重要。
基本用法:解析、修改与再序列化
README 提供了完整的可运行示例,下面完整保留并结合源码补充说明:
extern crate serde;
extern crate nu_json;
use nu_json::{Map, Value};
fn main() {
// Now let's look at decoding Hjson data
let sample_text = r#"
{
## specify rate in requests/second
rate: 1000
array:
[
foo
bar
]
}"#;
// Decode and unwrap.
let mut sample: Map<String, Value> = nu_json::from_str(&sample_text).unwrap();
// scope to control lifetime of borrow
{
// Extract the rate
let rate = sample.get("rate").unwrap().as_f64().unwrap();
println!("rate: {}", rate);
// Extract the array
let array: &mut Vec<Value> = sample.get_mut("array").unwrap().as_array_mut().unwrap();
println!("first: {}", array.first().unwrap());
// Add a value
array.push(Value::String("baz".to_string()));
}
// Encode to Hjson
let sample2 = nu_json::to_string(&sample).unwrap();
println!("Hjson:\n{}", sample2);
}
示例的执行路径覆盖了库的三大核心能力:
- 解码:
nu_json::from_str把含注释、无引号键值的 Hjson 文本解码为Map<String, Value>。Value枚举定义在 value.rs,共 8 个变体:Null、Bool(bool)、I64(i64)、U64(u64)、F64(f64)、String(String)、Array(Vec<Value>)、Object(Map<String, Value>)。 - 遍历与修改:
Map提供get/get_mut;Value提供as_f64(注意1000解码为整数后仍可转浮点读取)、as_array_mut等类型断言方法。示例中的作用域块({ ... })用于控制可变借用的生命周期,避免与外层sample的不可变持有冲突。 - 编码:
nu_json::to_string把修改后的结构再编码为 Hjson 文本,输出保留无引号等宽松风格。
此外,value.rs 还暴露了便捷的深层取值工具,如 find(按单层键取对象值)、find_path(按键数组逐级下钻)、以及遵循 RFC 6901 的 pointer(按 JSON Pointer 语法 /a/b 寻址);lib.rs 顶层则统一再导出 from_value / to_value 用于 Value 与任意 Serde 类型之间的互转。
反序列化入口:四种输入形态
lib.rs 从 de 模块再导出了一组解析入口,均定义在 de.rs:
| 函数 | 输入形态 | 位置 |
|---|---|---|
from_str |
&str 字符串切片 |
de.rs#L822 |
from_slice |
&[u8] 字节切片 |
de.rs#L814 |
from_reader |
任意 Read 实现(文件、管道等) |
de.rs#L802 |
from_iter |
字节迭代器 | de.rs#L763 |
同时导出的 Deserializer 与 StreamDeserializer 提供了低层能力:前者允许在实现 Deserialize trait 时获得细粒度的流式解析控制,后者用于处理以换行分隔的 Hjson 数据流。这四个函数式入口与两个低层类型覆盖了从“一行代码解析”到“逐 token 流式处理”的全部需求。
序列化入口与格式化控制
lib.rs 从 ser 模块再导出的写入口定义在 ser.rs,可归纳为三组:
基础输出(紧凑 Hjson)
to_string(ser.rs#L997):内部先to_vec得到Vec<u8>再转String,README 示例即用它生成 Hjson 文本;to_vec/to_writer:输出到字节缓冲或任意Write实现;- 低层
Serializer(ser.rs#L15-L34):Serializer::new(writer)创建默认格式,Serializer::with_indent(writer, indent)传入自定义缩进字节。
缩进控制
to_string_with_indent(value, indent: usize)(ser.rs#L1008):用indent个空格缩进输出多行 Hjson;to_string_with_tab_indentation(value, tabs: usize)(ser.rs#L1019):用tabs个 Tab 缩进。
这两个带缩进的变体在 CHANGELOG 中有清晰的引入轨迹:0.59.1 加入 “Add indent flag to json (first draft)”,0.60.0 随后 “Adds tab indentation option for JSON files”——先空格后 Tab,分两步补齐了缩进能力。
原始紧凑输出
to_string_raw(ser.rs#L1031-L1040):实现注释写明 “And remove all whitespace”,其内部直接委托给serde_json::to_string,因此产出的不是 Hjson 而是标准的紧凑 JSON 单行文本。CHANGELOG 中0.42.0记录 “add in a raw flag in the command to json”——这个 API 正是 Nushellto json命令--raw参数的底层支撑,供需要将数据写回严格 JSON 消费方的场景使用。
错误报告:精确到行列号的语法错误
Hjson 的宽松语法意味着解析失败时更需要定位信息。error.rs 中的 Error 枚举提供三类错误:
pub enum Error {
/// The JSON value had some syntactic error.
Syntax(ErrorCode, usize, usize), // (错误码, 行, 列)
Io(io::Error),
FromUtf8(FromUtf8Error),
}
其 Display 实现把语法错误渲染为 {code:?} at line {line} column {col} 的形式(error.rs#L131-L133),例如 “expected : at line 3 column 8”。
行号与列号的来源是 util.rs 中的 StringReader:它在 next() 消费字节时累加 line / col 计数器(遇 \n 时行号加一、列号归零),并通过 pos() 返回当前位置。解析器在报错时调用 rdr.error(ErrorCode) 即可把错误钉死在具体的行和列上。
ErrorCode(error.rs#L17-L71)覆盖了全部典型失败场景,可按语义归为几组:
- EOF 类:
EofWhileParsingList/EofWhileParsingObject/EofWhileParsingString/EofWhileParsingValue——对应“EOF while parsing a list”等消息; - 期望字符类:
ExpectedColon(expected:)、ExpectedListCommaOrEnd(expected,or])、ExpectedObjectCommaOrEnd(expected,or}); - 值与标识符类:
ExpectedSomeIdent(期望true/false/null等字面量)、ExpectedSomeValue、InvalidNumber; - 转义与 Unicode 类:
InvalidEscape、InvalidUnicodeCodePoint、LoneLeadingSurrogateInHexEscape、UnexpectedEndOfHexEscape; - Hjson 特有:
KeyMustBeAString与前述的PunctuatorInQlString——后者是严格 JSON 解析器中不存在的、专为无引号字符串词法增设的错误码; - 兜底:
Custom(String)承载自由文本消息,TrailingCharacters报告值之后的非空白尾随字符。
与 Nushell 的集成:nu-protocol 特性与测试夹具
nu-json 之所以在 Nushell 仓库中维护,核心是它作为 shell 数据管线的 JSON/Hjson 引擎。两处源码证据:
-
可选的
nu-protocol依赖。Cargo.toml 声明nu-protocol = { workspace = true, optional = true },而 lib.rs 中:#[cfg(feature = "nu-protocol")] mod nu_value;从源码结构看,
nu_value.rs仅在启用该 feature 时编译,承担 nu-json 的Value与 NushellValue之间的类型适配;这也解释了 README 文档链接说明——serde-hjson / serde_json 的既有文档对 nu-json 同样适用,因为核心数据结构一脉相承。 -
测试夹具直接消费 .hjson 文件。仓库顶层的
tests/assets/nu_json目录包含 59 个.hjson文件、46 个.json文件与 1 个.txt文件(见 tests/assets/nu_json),这些夹具被 Nushell 的集成测试用于验证from json/to json等命令对两种语法风格的解析与输出行为;crate 自身的单元测试入口在 tests/main.rs。
演进脉络:从分叉到独立 crate
CHANGELOG 按 Keep a Changelog 格式与语义化版本管理,几个关键节点勾勒出 nu-json 的能力生长史:
| 版本 | 要点 |
|---|---|
| 0.22.0(2020-11-22) | 分叉自 serde-hjson,正式加入 Nu 项目工作区,版本对齐父项目 |
| 0.28.0(2021-03-09) | 序列化/反序列化默认保序(引入 preserve_order 语义) |
| 0.42.0(2021-12-28) | to json 命令新增 raw 参数,修复 to json -r 序列化日期时缺少空格的问题 |
| 0.59.1 / 0.60.0(2022-03) | 先后加入 indent(空格缩进)与 Tab 缩进选项 |
| 0.66.0(2022-07-26) | 修复解析含超大数字的 JSON 时的 panic |
| 0.67.0(2022-08-16) | 以 fancy-regex 替换 regex crate |
| 0.73.0(2022-12-20) | lazy_static 替换为 once_cell |
| 0.76.0(2023-02-21) | 禁用该 crate 的自动 benchmark harness(与 Cargo.toml 中 [lib] bench = false 对应) |
可以看到,这个 crate 的迭代既包含 Hjson 解析器本身的健壮性修复(大数字 panic、JSON 解析修复),也持续跟进 Nushell 命令层的需求(raw、indent、tab 缩进),是一个“引擎 crate 随 shell 命令能力共同演化”的典型例子。
小结
nu-json 在 Nushell 体系中的角色可以概括为三点:一是作为 serde-hjson 的持续维护分叉,提供带注释、无引号键值等 Hjson 宽松语法的解析与生成能力;二是通过 preserve_order 默认特性与 LinkedHashMap 保证配置数据的键序可读性;三是通过可选的 nu-protocol 特性把自身 Value 模型桥接进 Nushell 的管道系统,并由 tests/assets/nu_json 下的 100 余个夹具文件持续回归验证。对于需要在 Rust 项目中读写“人类友好 JSON”、或需要紧凑严格 JSON 输出(to_string_raw)的场景,这套 API 与错误定位机制都已相当完备。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00