首页
/ nu-json 深度解析:Nushell 的人本 JSON(Hjson)解析与序列化库

nu-json 深度解析:Nushell 的人本 JSON(Hjson)解析与序列化库

2026-09-05 13:09:31作者:柯茵沙

本文基于 Nushell 仓库中的 nu-json crate 文档及其源码实现,系统讲解这个 Human JSON(Hjson)库的定位、依赖配置、解析与序列化的完整 API、错误报告机制,以及它如何通过 preserve_ordernu-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 项目开发者;
  • 核心依赖为 serdeserde_jsonnum-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.rsValue 枚举同时提供 I64U64F64 三个数字变体相呼应,保留了整数的精确性而不必一律转浮点。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);
}

示例的执行路径覆盖了库的三大核心能力:

  1. 解码nu_json::from_str 把含注释、无引号键值的 Hjson 文本解码为 Map<String, Value>Value 枚举定义在 value.rs,共 8 个变体:NullBool(bool)I64(i64)U64(u64)F64(f64)String(String)Array(Vec<Value>)Object(Map<String, Value>)
  2. 遍历与修改Map 提供 get / get_mutValue 提供 as_f64(注意 1000 解码为整数后仍可转浮点读取)、as_array_mut 等类型断言方法。示例中的作用域块({ ... })用于控制可变借用的生命周期,避免与外层 sample 的不可变持有冲突。
  3. 编码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.rsde 模块再导出了一组解析入口,均定义在 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

同时导出的 DeserializerStreamDeserializer 提供了低层能力:前者允许在实现 Deserialize trait 时获得细粒度的流式解析控制,后者用于处理以换行分隔的 Hjson 数据流。这四个函数式入口与两个低层类型覆盖了从“一行代码解析”到“逐 token 流式处理”的全部需求。

序列化入口与格式化控制

lib.rsser 模块再导出的写入口定义在 ser.rs,可归纳为三组:

基础输出(紧凑 Hjson)

  • to_stringser.rs#L997):内部先 to_vec 得到 Vec<u8> 再转 String,README 示例即用它生成 Hjson 文本;
  • to_vec / to_writer:输出到字节缓冲或任意 Write 实现;
  • 低层 Serializerser.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):用 tabsTab 缩进。

这两个带缩进的变体在 CHANGELOG 中有清晰的引入轨迹:0.59.1 加入 “Add indent flag to json (first draft)”,0.60.0 随后 “Adds tab indentation option for JSON files”——先空格后 Tab,分两步补齐了缩进能力。

原始紧凑输出

  • to_string_rawser.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 正是 Nushell to 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) 即可把错误钉死在具体的行和列上。

ErrorCodeerror.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 等字面量)、ExpectedSomeValueInvalidNumber
  • 转义与 Unicode 类InvalidEscapeInvalidUnicodeCodePointLoneLeadingSurrogateInHexEscapeUnexpectedEndOfHexEscape
  • Hjson 特有KeyMustBeAString 与前述的 PunctuatorInQlString——后者是严格 JSON 解析器中不存在的、专为无引号字符串词法增设的错误码;
  • 兜底Custom(String) 承载自由文本消息,TrailingCharacters 报告值之后的非空白尾随字符。

与 Nushell 的集成:nu-protocol 特性与测试夹具

nu-json 之所以在 Nushell 仓库中维护,核心是它作为 shell 数据管线的 JSON/Hjson 引擎。两处源码证据:

  1. 可选的 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 与 Nushell Value 之间的类型适配;这也解释了 README 文档链接说明——serde-hjson / serde_json 的既有文档对 nu-json 同样适用,因为核心数据结构一脉相承。

  2. 测试夹具直接消费 .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 与错误定位机制都已相当完备。

登录后查看全文
热门项目推荐
相关项目推荐