typetag

Serde serializable and deserializable trait objects

  • Owner: dtolnay/typetag
  • Platform:
  • License:: Apache License 2.0
  • Category::
  • Topic:
  • Like:
    0
      Compare:

Github stars Tracking Chart

Typetag




Serde serializable and deserializable trait objects.

This crate provides a macro for painless serialization of &dyn Trait trait
objects and serialization + deserialization of Box<dyn Trait> trait objects.

Let's dive into the example and I'll explain some more below.

[dependencies]
typetag = "0.1"

Supports rustc 1.31+

Example

Suppose I have a trait WebEvent and I require that every implementation of the
trait be serializable and deserializable so that I can send them to my
ad-serving AI. Here are just the types and trait impls to start with:

trait WebEvent {
    fn inspect(&self);
}

#[derive(Serialize, Deserialize)]
struct PageLoad;

impl WebEvent for PageLoad {
    fn inspect(&self) {
        println!("200 milliseconds or bust");
    }
}

#[derive(Serialize, Deserialize)]
struct Click {
    x: i32,
    y: i32,
}

impl WebEvent for Click {
    fn inspect(&self) {
        println!("negative space between the ads: x={} y={}", self.x, self.y);
    }
}

We'll need to be able to send an arbitrary web event as JSON to the AI:

fn send_event_to_money_factory(event: &dyn WebEvent) -> Result<()> {
    let json = serde_json::to_string(event)?;
    somehow_send_json(json)?;
    Ok(())
}

and receive an arbitrary web event as JSON on the server side:

fn process_event_from_clickfarm(json: &str) -> Result<()> {
    let event: Box<dyn WebEvent> = serde_json::from_str(json)?;
    overanalyze(event)?;
    Ok(())
}

The introduction claimed that this would be painless but I'll let you be the
judge.

First stick an attribute on top of the trait.

#[typetag::serde(tag = "type")]
trait WebEvent {
    fn inspect(&self);
}

Then stick a similar attribute on all those impl blocks too.

#[typetag::serde]
impl WebEvent for PageLoad {
    fn inspect(&self) {
        println!("200 milliseconds or bust");
    }
}

#[typetag::serde]
impl WebEvent for Click {
    fn inspect(&self) {
        println!("negative space between the ads: x={} y={}", self.x, self.y);
    }
}

And now it works as described. All in all, three lines were added!

What?

Trait objects are serialized by this library like Serde enums. Every impl of the
trait (anywhere in the program) looks like one variant of the enum.

All three of Serde's tagged enum representations are supported. The one shown
above is the "internally tagged" style so our two event types would be
represented in JSON as:

{"type":"PageLoad"}
{"type":"Click","x":10,"y":10}

The choice of enum representation is controlled by the attribute that goes on
the trait definition. Let's check out the "adjacently tagged" style:

#[typetag::serde(tag = "type", content = "value")]
trait WebEvent {
    fn inspect(&self);
}
{"type":"PageLoad","value":null}
{"type":"Click","value":{"x":10,"y":10}}

and the "externally tagged" style, which is Serde's default for enums:

#[typetag::serde]
trait WebEvent {
    fn inspect(&self);
}
{"PageLoad":null}
{"Click":{"x":10,"y":10}}

Separately, the value of the tag for a given trait impl may be defined as part
of the attribute that goes on the trait impl. By default the tag will be the
type name when no name is specified explicitly.

#[typetag::serde(name = "mouse_button_down")]
impl WebEvent for Click {
    fn inspect(&self) {
        println!("negative space between the ads: ({}, {})", self.x, self.y);
    }
}
{"type":"mouse_button_down","x":10,"y":10}

Conceptually all you're getting with this crate is that we build for you an enum
in which every impl of the trait in your program is automatically registered as
an enum variant. The behavior is the same as if you had written the enum
yourself and implemented Serialize and Deserialize for the dyn Trait object in
terms of the enum.

// generated (conceptually)
#[derive(Serialize, Deserialize)]
enum WebEvent {
    PageLoad(PageLoad),
    Click(Click),
    /* ... */
}

So many questions

  • Does it work if the trait impls are spread across different crates? Yes

    Serialization and deserialization both support every single impl of the trait
    across the dependency graph of the final program binary.

  • Does it work in non-self-describing data formats like Bincode? Yes

    All three choices of enum representation will round-trip correctly through
    compact binary formats including Bincode.

  • Does it support non-struct types? Yes

    The implementations of the trait can be structs, enums, primitives, or
    anything else supported by Serde. The Serialize and Deserialize impls may be
    derived or handwritten.

  • Didn't someone explain to me why this wasn't possible? Yes

    It might have been me.

  • Then how does it work?

    We use the inventory crate to produce a registry of impls of your trait,
    which is built on the ctor crate to hook up initialization functions that
    insert into the registry. The first Box<dyn Trait> deserialization will
    perform the work of iterating the registry and building a map of tags to
    deserialization functions. Subsequent deserializations find the right
    deserialization function in that map. The erased-serde crate is also
    involved, to do this all in a way that does not break object safety.

License

Main metrics

Overview
Name With Ownerdtolnay/typetag
Primary LanguageRust
Program languageRust (Language Count: 1)
Platform
License:Apache License 2.0
所有者活动
Created At2019-01-21 08:14:33
Pushed At2025-08-23 01:24:06
Last Commit At2025-08-22 18:20:52
Release Count30
Last Release Name0.2.20 (Posted on 2025-02-28 02:41:03)
First Release Name0.1.0 (Posted on 2019-01-23 09:42:18)
用户参与
Stargazers Count1.4k
Watchers Count8
Fork Count42
Commits Count274
Has Issues Enabled
Issues Count60
Issue Open Count9
Pull Requests Count33
Pull Requests Open Count1
Pull Requests Close Count5
项目设置
Has Wiki Enabled
Is Archived
Is Fork
Is Locked
Is Mirror
Is Private