- Published on
serde自定义序列化
- Authors
- Name
- ttyS3
serde 几乎是目前 Rust 生态中最常用的序列化与反序列化库了.
Golang 实现
作为一个 Golang 程序员来说, 免不了要对比一下.
Golang 官方库直接实现了 json 的序列化和反序列化.
对于序列化和反序列化, Go 都是用一个简单的接口interface
表示:
// https://pkg.go.dev/encoding/json#Marshaler
type Marshaler interface {
MarshalJSON() ([]byte, error)
}
// https://pkg.go.dev/encoding/json#Unmarshaler
type Unmarshaler interface {
UnmarshalJSON([]byte) error
}
注意这个接口只是针对 JSON 的, 接口名后缀JSON
是有意义的. 因为对于同一个数据类型, 它可能需要实现很多种格式的序列化, 比如 yaml, toml 等. 如果按这个命名来, 它可能是: MarshalYaml
, MarshalToml
看上去很简单是吧? 嗯, 是挺简单的. 但是其实这里是有坑的, 或者说, 实现的时候一定要注意一些细节.
Marshaler
的实现一定要用普通的receiver, (即不要只实现 pointer receiver的), 因为pointer only 会导致如果是非指针形式的时候, 在序列化的时候无法调用到我们自己实现的方法.
Unmarshaler
的实现一定要用 pointer receiver, 因为是解析数据到自身, 因此一定要修改自身, 不可修改是没有意义的.
举个例子, 假设我们想要JSON序列化一个自定义的int类型(主要用于作为enum的功能来用)为某些特定的string(主要用于JSON结构化日志的时候, 阅读更友好):
type HideType int
const (
HideTypeNone HideType = 0
HideTypeLocation HideType = 1 << 0
HideTypeAge HideType = 1 << 1
HideTypeSex HideType = 1 << 2
HideTypeNation HideType = 1 << 3
)
var _hideTypeValuesMap = map[HideType]string{
HideTypeNone: "none",
HideTypeLocation: "location",
HideTypeAge: "age",
HideTypeSex: "sex",
HideTypeNation: "nation",
}
var _hideTypeValueToType = map[string]HideType{
"none": HideTypeNone,
"location": HideTypeLocation,
"age": HideTypeAge,
"sex": HideTypeSex,
"nation": HideTypeNation,
}
func (ht HideType) String() string {
if val, ok := _hideTypeValuesMap[ht]; ok {
return val
}
return "unknown"
}
// MarshalJSON impl Marshaler interface https://pkg.go.dev/encoding/json#Marshaler
func (ht HideType) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf(`"%v"`, ht.String())), nil
}
// UnmarshalJSON impl Unmarshaler interface
// https://pkg.go.dev/encoding/json#Unmarshaler
func (ht *HideType) UnmarshalJSON(rawJSON []byte) error {
htStr := bytes.Trim(rawJSON, `"`)
if htInt, ok := _hideTypeValueToType[string(htStr)]; ok {
*ht = htInt
return nil
}
return fmt.Errorf("parse into HideType failed, unknown HideType: %v", htStr)
}
没错, 整个实现非常简单, 对于 MarshalJSON()
我们只需要返回我们想要返回的 JSON string 即可. 注意这里一定要返回合法的JSON string类型. 这里使用了 "%v"
, 而没有使用 %q
, 主要是因为在我们的使用场景(类似enum)下100%确定不会有需要再次转义, 如果不确定的场景, 最好还是要用 %q
.
UnmarshalJSON 我们由于确定输入是合法并且格式一定是特定的, 因此处理上也非常简单.不需要考虑过多.
Rust 实现
当然, serde 本身就可以处理这种序列化, 我们只需要加上#[derive(Serialize, Deserialize, Debug)]
即可. 这里特意用的自定义方式实现, 主要是从学习的角度.
序列化实现比较简单, Rust 里面我们只需要实现 Serialize
trait 即可:
pub trait Serialize {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer;
}
反序列化相对来说麻烦一些. 不像 Go 里面简单粗暴, 实现细节交给用户来处理, serde 里面需要按照 serde data model 来交换数据.
单从 Deserialize
trait 来看, 好像反序列化也差不多. 但是实际上它还需要一个 Vistor trait
pub trait Deserialize<'de>: Sized {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>;
}
从语法上来说, Visitor
trait 只有一个 expecting
方法是必须实现的, 但是实际使用中我们要根据具体的JSON 数据类型选择实现其它方法. 比如在我们这个例子中, 我们可以明确的确定, 我们收到的 JSON 数据是一个字符串类型的, 因此只需要visit_str
, 注意 visit_str
比起 Golang 里面要用户手动解析 JSON 数据, serde 里面已经默认把数据给我们解析出来了, 所以, 当输入是 "foo"
的时候, 在 serde 里面我们visit_str
得到的是 foo
, 而在 Go 里面我们由于得到的是 "foo"
, 因此还要自行处理外面的引号.
最后, 我们要将这个实现了 Visitor
trait 的类型, 绑定到我们要反序列化的类型的Deserialize
trait 上面: deserializer.deserialize_str(HideTypeVisitor)
pub trait Visitor<'de>: Sized {
/// The value produced by this visitor.
type Value;
/// Format a message stating what data this Visitor expects to receive.
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result;
/// ...
}
use std::collections::HashMap;
use std::fmt;
use serde::{Serialize, Deserialize, Serializer, Deserializer};
use serde::de::{Error, Unexpected, Visitor};
use once_cell::sync::Lazy;
[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
enum HideType {
HideTypeNone = 0,
HideTypeLocation = 1 << 0,
HideTypeAge = 1 << 1,
HideTypeSex = 1 << 2,
HideTypeNation = 1 << 3,
}
static FILTER_TYPE_TO_NAME: [(HideType, &str); 5] = [
(HideType::HideTypeNone, "none"),
(HideType::HideTypeLocation, "location"),
(HideType::HideTypeAge, "age"),
(HideType::HideTypeSex, "sex"),
(HideType::HideTypeNation, "nation"),
];
static TYPE_STR_MAP: Lazy<HashMap<HideType, &str>> = Lazy::new(|| {
let mut m = HashMap::new();
for (k, v) in FILTER_TYPE_TO_NAME.iter() {
m.insert(*k, *v);
}
m
});
static STR_TYPE_MAP: Lazy<HashMap<&str, HideType>> = Lazy::new(|| {
let mut m = HashMap::new();
for (k, v) in FILTER_TYPE_TO_NAME.iter() {
m.insert(*v, *k);
}
m
});
impl Serialize for HideType {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
return TYPE_STR_MAP.get(self).unwrap().serialize(serializer);
}
}
struct HideTypeVisitor;
impl<'de> Visitor<'de> for HideTypeVisitor {
type Value = HideType;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str(r#"a string in one of: none, location, age, sex, nation"#)
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: Error,
{
match STR_TYPE_MAP.get(v) {
Some(t) => Ok(*t),
None => Err(E::invalid_value(Unexpected::Str(v), &self)),
}
}
}
impl<'de> Deserialize<'de> for HideType {
fn deserialize<D>(deserializer: D) -> Result<HideType, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(HideTypeVisitor)
}
}
当然, 这里也并不一定要用到 hashmap, 直接用 match 处理从类型到 string的转换也是可以的. 不过通过 hashmap 的方式会比较自然, 并且我们不再需要手动去维护一个正向和反向的 map 了.
总体来说, Golang 里面实现自定义序列化和反序列化, 比较直接和简单粗暴. 整个接口的定义也非常简单. 而 Rust 里的 serde 则有很多功能和功能. 首先你一定要了解的是 serde data model. 相比于 Golang 里面直接用 Go 的类型, 并且其默认实现直接将 Go 的类型, 比如 []byte 绑定到其实现上 ([]byte类型的数据在序列化后会被Golang转换成base64的表现形式). serde 里面是通过 serde data model 来实现数据映射的. 而 serde 本身是一个框架, 并不负责实现, 其实现由扩展提供, 如 serde_json, serde_yaml 等. 所以 serde 的好处是, trait 是统一的, 而 Go 里面则是各实现各的, 比如官方实现了 JSON, 第三方实现了 YAML 等.
比如 yaml:
UnmarshalYAML(value *Node) error
官方 JSON:
UnmarshalJSON([]byte) error
可以看到, 虽然命名和参数样子差不多, 但是实际上还是不一样, 当然, 官方也并没有任何规范说明要一样. 实际上也不能做成一样, 假设 json 和 yaml 的 Unmarshaler interface 都定义为 Unmarshal([]byte) error
, 由于 Go 的 interface 是鸭子类型, 这会导致错误的断言.
另外一点就是, 由于 serde 是一个框架, 而我们的自定义序列化也是映射到框架的数据模型, 因此是跟语言(json, yaml, toml 等)无关的. 可以做到一次定义, 到处运行. 而 Golang 的自定义序列化一定是针对某个类型的(比如 JSON), 上面的示例, 如果换成 yaml, 还得重新实现一次, 而 serde 则不存在这个问题.
2022-07-27 补充:
还有一点就是序列化字段重命名, golang 是通过给 struct 打 tag 的方式, 不同的语言需要不同的tag, 如何解析这个 tag 是序列化mod所负责的工作, 比如 json 用的是 `json`, yaml 用的是 `yaml`, 而数据库相关的 gorm 用的是 `gorm`, 因此从这一点来说, golang 的 struct tag 并不是专门用于序列化. 还可以用在其它地方.
而 Rust 的字段重命名是在框架层面的, 也就是说, 指定了rename后, 无论是 json 还是 yaml 都会按照这个来, 优点是不必要重复地添加很多 tag. (对比: golang 一个 struct 要同时加上 json, yaml, toml 甚至 bson 的 tag, 就显得很没有可读性, 但是如果你不加, 有些字段可能序列化出来不是你要的结果, 比如 ID 在 mongodb 里面要用 `_id` )
Refs
https://pkg.go.dev/encoding/json#Marshaler
https://pkg.go.dev/encoding/json#Unmarshaler
https://serde.rs/data-model.html