Serde 字符串映射读入 Option 报 expected option
Serde 的字符串值反序列化器不会自动把文本包装成 Some。为纯文本映射限定字段转换,分别保留缺失键、空字符串和普通值的含义,不把 JSON null 混进同一规则。
将字符串键值对交给 Serde 的 MapDeserializer,结构体里的普通字符串能读取,换成 Option<String> 却可能报错:
invalid type: string "value", expected option
例如映射中确实有 ("optional", "value"),目标字段是 optional: Option<String>。这与“键没有出现”不同;错误发生在已经拿到字符串、准备把它读进可选字段时。该组合可见于 Serde 1.0.228,正式 1.0.229 中相关字符串与 Option 读取规则仍相同。
有字符串,不代表已经提供了 Some
MapDeserializer 将键和值各自转换成反序列化器。这里的 &str 进入 StrDeserializer,它会把字符串交给 visitor;而 Option 的 visitor 需要先收到 visit_some、visit_none 或相应的空值入口,再决定是否读取内部值。
因此,“目标是 Option”不意味着任意底层字符串反序列化器都会自动调用 visit_some。JSON 解析器可以为自身格式实现这种适配,也不代表纯 Rust 字符串映射具有同样的格式语义。
直接把迭代器的值改为 Some("value"),还要求该值能转换成匹配的反序列化器,不能当作现成可用的替换。给整个结构体套 flatten 又会改变所走的中间路径,不宜只为绕过一个字段错误而改变全部字段的读取方式。
纯文本字段,明确把存在的值读成字符串
若输入契约就是“键缺失代表 None,键存在时必须是字符串”,可以只为这个字段增加转换函数。下面是启用 Serde derive、使用 Rust 2021 edition 的完整示例:
use serde::de::value::{Error, MapDeserializer};
use serde::{Deserialize, Deserializer};
fn present_text<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
String::deserialize(deserializer).map(Some)
}
#[derive(Debug, Deserialize)]
struct Input {
#[serde(default, deserialize_with = "present_text")]
optional: Option<String>,
}
fn main() -> Result<(), Error> {
let pairs = [("optional", "value")];
let input = Input::deserialize(MapDeserializer::<_, Error>::new(
pairs.into_iter(),
))?;
println!("{:?}", input.optional);
Ok(())
}
存在的字段先按 String 读取,再包装成 Some;示例预期打印 Some("value")。#[serde(default)] 则负责字段缺失的另一条路径,使用 Option 的默认值 None。两项作用不同,添加自定义读取函数后不应漏掉缺失字段的处理。
这个规则保留空字符串:("optional", "") 应得到 Some(""),而不是 None。空白、字符串 "null"、文本 "None" 也不会被擅自当成缺失值。若业务要求这些特殊含义,应另外定义输入契约,不能在转换函数里一概丢掉。
不要顺手改变 JSON 的空值约定
该函数要求存在的值能反序列化成 String。把同一个结构体用于 JSON 时,显式 null 不会因此自动得到 None;它将进入 String 的类型校验并报错。若原接口允许 null,应保留原来的 JSON DTO,或为不同输入格式安排各自的转换入口,避免全局修改共享模型。
同样不应先把数字、布尔值等任意输入调用 to_string() 再传入,借此掩盖上游类型错误。这里解决的是已确认纯文本映射的可选字段读取,不是放宽全部输入类型。
回查普通值、缺失键和空字符串,再检查其他必填字段仍会拒绝缺失或错误值。若模型同时服务多种格式,还要覆盖这些格式原有的 null 与错误类型行为。只让一条带值输入成功,无法证明可选字段的完整约定没有变化。


