Published on

serde自定义序列化

Authors
  • avatar
    Name
    ttyS3
    Twitter

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

https://serde.rs/impl-serialize.html

https://serde.rs/impl-deserialize.html