升级到 1.0.229 与 1.0.154
主线位置:不在某一条主线上,而是看两条主线的两端各变了什么。serde 一侧的改动落在 derive 生成的反序列化代码(反序列化主线的
visit_enum);serde_json 一侧的改动落在解析器报错的位置(反序列化主线的parse_str)和几个本站没开的 feature 上。
flowchart TB
item["一条改动"] --> feat{"本站开了这个 feature"}
feat -->|没开| none1["不影响"]
feat -->|开了或无条件| api{"本站走到这段代码"}
api -->|没走到| none2["不影响"]
api -->|走到| run["跑两遍对比输出"]
本站的提交 89a61b1 锁的是 serde 1.0.228、serde_json 1.0.150。两段区间里每一条改动是什么,怎么读这一卷的「锁定版本」一节已经逐条列过,这里不再重复,只加一列:对本站有没有影响,凭什么这么说。
先看本站开了哪些 feature
serde_json 的五条改动里有三条只在某个 feature 下生效,所以先查 feature。在 89a61b1 的一份拷贝里离线跑(--locked 不改 Cargo.lock),服务端构建和 wasm 客户端构建各一次:
cargo tree --offline --locked -p app --features server -e normal -f '{p} [{f}]' --prefix none --color never \
| grep -E '^serde_(json|yaml) v' | sed 's/ (\*)//' | sort -u
cargo tree --offline --locked -p app --target wasm32-unknown-unknown -e normal -f '{p} [{f}]' --prefix none --color never \
| grep -E '^serde_(json|yaml) v' | sed 's/ (\*)//' | sort -u
两次输出相同:
serde_json v1.0.150 [default,raw_value,std]
serde_yaml v0.9.34+deprecated []
本站自己的 Cargo.toml 只写了 serde_json = "1.0",没开任何 feature。raw_value 是别人带进来的,cargo tree -e features -i serde_json 列出了是谁:
├── serde_json feature "raw_value"
│ ├── axum v0.8.9 (*)
│ ├── sqlx-core v0.8.6 (*)
│ └── sqlx-postgres v0.8.6 (*)
arbitrary_precision、preserve_order、unbounded_depth、float_roundtrip 都没开。站点代码里也搜不到 RawValue、as_f32、arbitrary_precision、preserve_order。
serde 1.0.228 → 1.0.229
| 改动 | 本站 | 依据 |
|---|---|---|
| syn 2 → 3(#3085) | 不新增 crate | 本站的 Cargo.lock 里已经有 syn 3.0.6(async-trait 0.1.92 用它);升级后 serde_derive 的依赖从 syn 2.0.118 换成 syn 3.0.6,syn 2 仍被其他过程宏使用 |
生成代码少用 ?(#2936) | 10 个 enum 的生成代码换了写法,行为不变 | 见下一节的展开对比 |
serde_derive 最低编译器 1.61 → 1.71 | 不影响 | 镜像用 rust:1-trixie(当前稳定版),本机 rustc 1.99.0 |
减少 serde_derive 自身的 llvm-lines(#3034 等) | 不影响 | 只改 serde_derive 自己编译出来的 IR,不改生成的代码 |
seq_impl! 去掉 mut_mut、unstable 的 never_type | 不影响 | 前者行为不变;unstable 本站没开(serde 的 feature 是 alloc,default,derive,rc,serde_derive,std) |
私有模块 __private228 → __private229 | 不影响 | 只出现在生成代码里,站点代码没有直接写它 |
#2936 落在本站的哪些类型上
#2936 改的是外部标签 enum 的 visit_enum、相邻标签 enum 的 visit_seq 和 deserialize_in_place 的 visit_seq。本站没有相邻标签,也没有开 serde_derive 的 deserialize_in_place;派生了 Deserialize 的外部标签 enum 有 10 个:PayError、PayAction、LessonKind、Region、ProductKind、Language、LlmFailureAction、ModerationLabel、LlmProvider、LlmRole。内部标签的 LlmContentBlock 不在其中。
挑 LlmFailureAction 和 ModerationThresholdsConfig,原样抄进两个 crate,一个依赖 serde = "=1.0.229"、serde_json = "=1.0.154",一个依赖 serde = "=1.0.228"、serde_json = "=1.0.150":
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum LlmFailureAction {
#[default]
Review,
Reject,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ModerationThresholdsConfig {
pub block_above: Option<f32>,
pub flag_above: Option<f32>,
}
fn main() {
let a: LlmFailureAction = serde_json::from_str("\"reject\"").unwrap();
println!("{a:?} {}", serde_json::to_string(&a).unwrap());
let t: ModerationThresholdsConfig = serde_json::from_str(r#"{"flag_above":0.5}"#).unwrap();
println!("{t:?}");
}
两边的输出相同:
Reject "reject"
ModerationThresholdsConfig { block_above: None, flag_above: Some(0.5) }
cargo expand 的结果:1.0.229 一侧 586 行,1.0.228 一侧 583 行。把 1.0.228 一侧的 __private228 换成 __private229 再 diff,只剩 14 行,全在 LlmFailureAction 的 visit_enum 里;两个 Serialize impl 和 ModerationThresholdsConfig 的 Deserialize 一字不差:
212,213c212,213
< match _serde::de::EnumAccess::variant(__data)? {
< (__Field::__field0, __variant) => {
---
> match _serde::de::EnumAccess::variant(__data) {
> _serde::__private229::Ok((__Field::__field0, __variant)) => {
217c217
< (__Field::__field1, __variant) => {
---
> _serde::__private229::Ok((__Field::__field1, __variant)) => {
219a220,222
> }
> _serde::__private229::Err(__err) => {
> _serde::__private229::Err(__err)
1.0.229 一侧完整的 visit_enum:
fn visit_enum<__A>(
self,
__data: __A,
) -> _serde::__private229::Result<Self::Value, __A::Error>
where
__A: _serde::de::EnumAccess<'de>,
{
match _serde::de::EnumAccess::variant(__data) {
_serde::__private229::Ok((__Field::__field0, __variant)) => {
_serde::de::VariantAccess::unit_variant(__variant)?;
_serde::__private229::Ok(LlmFailureAction::Review)
}
_serde::__private229::Ok((__Field::__field1, __variant)) => {
_serde::de::VariantAccess::unit_variant(__variant)?;
_serde::__private229::Ok(LlmFailureAction::Reject)
}
_serde::__private229::Err(__err) => {
_serde::__private229::Err(__err)
}
}
}
variant(__data)? 那个 ? 并进了 match,多出一个原样返回错误的 Err 分支;每个变体里的 unit_variant(__variant)? 没动。对应 serde_derive 的 enum_externally.rs 第 72 行。? 隐含的 From::from 在这里是同一个错误类型之间的转换,去掉它不改变任何返回值,所以 10 个 enum 读到的结果和报错都和以前一样。外部标签 enum 的 visit_enum 其余部分见外部标签。
serde_json 1.0.150 → 1.0.154
| 版本 | 改动 | 本站 | 依据 |
|---|---|---|---|
| 1.0.151 | #1331 RawValue::from_string_unchecked | 不影响 | raw_value 开着(axum、sqlx 带进来),但这是新增的函数,站点和这两个依赖在锁定版本里都不调它也不可能调(1.0.150 里没有) |
| 1.0.152 | #1346 arbitrary_precision 下保留 -0 | 不影响 | 没开 arbitrary_precision;改的是 #[cfg(feature = "arbitrary_precision")] 的 parse_any_number |
| 1.0.152 | #1347 Value == f32 对超出 f32 范围的数为假 | 不影响 | 站点没有 Value 和 f32 的比较;读阈值走的是 f32 的 Deserialize,不经 Number::as_f32 |
| 1.0.153 | #1345 非法 UTF-8 的报错位置 | 报错文本变了 | 见下一节 |
| 1.0.154 | #1333 preserve_order 不再要求 std | 不影响 | 没开 preserve_order;它新增的可选依赖 foldhash 不会进本站的 Cargo.lock |
唯一看得见的变化:非法 UTF-8 的列号
#1345 改的是 read.rs 的 as_str,它在字符串的字节不是合法 UTF-8 时报 invalid unicode code point。这个函数不受任何 feature 控制,from_slice 读字节输入时都会走到;from_str 的输入已经是 &str,不会出现非法 UTF-8。机制和 PR 的来历见 serde_json 反序列化一侧。
本站用 from_slice 读外部字节的地方,是 dioxus server 函数的请求体(dioxus-fullstack 0.7.9 src/magic.rs 第 551 行)。上一页的 server 函数实验,在两套版本下跑出来只有一行不同:
< 服务端 字符串里有非法 UTF-8: Err(invalid unicode code point at line 1 column 16)
---
> 服务端 字符串里有非法 UTF-8: Err(invalid unicode code point at line 1 column 11)
输入是 {"name":"a\xff.png","data_base64":"x"},\xff 是第 11 个字节,第 16 个字节是 .png 后面的引号。1.0.150 报的是字符串读完时的位置,1.0.154 报的是第一个坏字节。对本站来说,这只改变 server 函数回给客户端的那条 500 错误里的列号;站点的测试里没有断言这个列号(crates/ 里搜不到 invalid unicode 或 column 加数字的断言)。
实际升一次
上面是逐条推理。再在 89a61b1 的另一份拷贝里真的升一次(只在拷贝里做,不动站点仓库):
cargo update --offline -p serde --precise 1.0.229
cargo update --offline -p serde_json --precise 1.0.154
Updating serde v1.0.228 -> v1.0.229
Updating serde_core v1.0.228 -> v1.0.229
Updating serde_derive v1.0.228 -> v1.0.229
note: pass `--verbose` to see 203 unchanged dependencies behind latest
Updating serde_json v1.0.150 -> v1.0.154
note: pass `--verbose` to see 202 unchanged dependencies behind latest
Cargo.lock 的 diff 只有四个包的版本号和校验和,外加 serde_derive 的依赖那一行:
6845c6845
< "syn 2.0.118",
---
> "syn 3.0.6",
没有新增或删除任何包。升级前后各跑一次 app-core 和 llm 两个 crate 的单元测试(site.json 的解析、LLM 请求体的形状都在里面),结果相同:
test result: ok. 112 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 44 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
(这是升级后的一次;升级前的一次两行相同,只有 finished in 的耗时是 0.01s 和 0.02s。)
上一页的五个实验程序(site.json 的解析、经 Value 写回、LLM 的请求与响应、前置元数据、server 函数)也在两套版本下各跑了一次,diff 的结果是四个完全相同,server_fn 只差上面那一行。
问: 本站的 Cargo.toml 写的是 serde = "1.0",1.0.229 早就发布了,为什么构建出来还是 1.0.228?要不要把 serde_derive 也写进 Cargo.toml 一起升?
答: "1.0" 只是允许的范围,实际用哪个版本由 Cargo.lock 决定,cargo build 不会自己改锁文件,要 cargo update 才会动。serde_derive 不用单独写:cargo update -p serde --precise 1.0.229 一条命令就把 serde_core、serde_derive 一起带到了 1.0.229(上面的输出)。这是因为 serde_core 用一个永远不成立的 cfg(any()) 依赖精确锁住了 serde_derive = "=1.0.229",三者只能同版本,见源码地图的「版本怎么锁在一起」。
小结
- 本站的 serde_json 开了
default、std、raw_value,raw_value来自 axum 和 sqlx;arbitrary_precision、preserve_order都没开。 - serde 1.0.229:唯一改变生成代码的 #2936 落在本站 10 个外部标签 enum 的
visit_enum上,?并进match,行为不变;LlmFailureAction两版展开除私有模块名外只差 14 行。syn 3 本站已有,不新增 crate;最低编译器 1.71 不是问题。 - serde_json 1.0.154:五条改动里四条在本站不生效(feature 没开或 API 没用);#1345 让 server 函数遇到非法 UTF-8 时报的列号从字符串结尾移到第一个坏字节。
- 实际升级只改
Cargo.lock里四个包的版本和serde_derive的 syn 依赖;两个 crate 的 156 个单元测试和五个实验程序在升级前后结果相同,只有那一个列号不同。