11

I'm trying to do struct serialization, in which the bytes would eventually be sent down a pipe, reconstructed and methods be called on them.

I created a trait these structs would implement as appropriate and I'm using serde and serde-cbor for serialization:

extern crate serde_cbor;
#[macro_use]
extern crate serde_derive;
extern crate serde;

use serde_cbor::ser::*;
use serde_cbor::de::*;

trait Contract {
    fn do_something(&self);
}

#[derive(Debug, Serialize, Deserialize)]
struct Foo {
    x: u32,
    y: u32,
}

#[derive(Debug, Serialize, Deserialize)]
struct Bar {
    data: Vec<Foo>,
}

#[derive(Debug, Serialize, Deserialize)]
struct Baz {
    data: Vec<Foo>,
    tag: String,
}

impl Contract for Bar {
    fn do_something(&self) {
        println!("I'm a Bar and this is my data {:?}", self.data);
    }
}

impl Contract for Baz {
    fn do_something(&self) {
        println!("I'm Baz {} and this is my data {:?}", self.tag, self.data);
    }
}

fn main() {
    let data = Bar { data: vec![Foo { x: 1, y: 2 }, Foo { x: 3, y: 4 }, Foo { x: 7, y: 8 }] };
    data.do_something();

    let value = to_vec(&data).unwrap();
    let res: Result<Contract, _> = from_reader(&value[..]);
    let res = res.unwrap();
    println!("{:?}", res);
    res.do_something();
}

When I try to reconstruct the bytes using the trait as the type (given that I wouldn't know which underlying object is being sent), the compiler complains that the trait does not implement the Sized trait:

error[E0277]: the trait bound `Contract: std::marker::Sized` is not satisfied
  --> src/main.rs:52:15
   |
52 |     let res: Result<Contract, _> = from_reader(&value[..]);
   |              ^^^^^^^^^^^^^^^^^^^ the trait `std::marker::Sized` is not implemented for `Contract`
   |
   = note: `Contract` does not have a constant size known at compile-time
   = note: required by `std::result::Result`

I guess it makes sense since the compiler doesn't know how big the struct is supposed to be and doesn't know how to line up the bytes for it. If I change the line where I deserialize the object to specify the actual struct type, it works:

let res: Result<Bar, _> = from_reader(&value[..]);

Is there a better pattern to achieve this serialization + polymorphism behavior?

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Dash83
  • 1,357
  • 2
  • 17
  • 31
  • 2
    I... don't think you can do that. You can't recover the struct unless you know its concrete type, and you can't call methods on it unless you have a pointer to its vtable -- which you can't figure out unless you have access to its concrete type. Can you serialize a vtable? – trent Feb 22 '17 at 14:00
  • Seems to be the case, but I was hoping someone would point out something I'm missing. I have a non-idiomatic solution for this but adds coupling to the code... so I'm looking for something better. – Dash83 Feb 22 '17 at 14:09
  • 3
    Are you sure you want polymorphism and not simply an enum? Do you need your code to work with user supplied types? – oli_obk Feb 22 '17 at 16:12
  • I.... you know... but....no. You are correct, @ker. The "non-idiomatic" solution I had becomes far more natural when using enums with data associated to them. I keep trying to use enums as standard C enums, but I can change my design to use enums. If you post your suggestions as an answer, I'll accept it. – Dash83 Feb 22 '17 at 17:42
  • 1
    What about deserializing into an implementation that also implements `Into` for all other `Contract` implementations? – w.brian Feb 22 '17 at 21:45
  • @w.brian, that sounds plausible, but I ended up going with switching my design to using an enum of types implementing Contract with an associated struct of the corresponding type, that serializes flawlessly and then I do pattern matching on the enum and operate on the associated data. That was probably a better design from the start, but sometimes you have to go the wrong way first. – Dash83 Feb 23 '17 at 12:30

3 Answers3

10

It looks like you fell into the same trap that I fell into when I moved from C++ to Rust. Trying to use polymorphism to model a fixed set of variants of a type. Rust's enums (similar to Haskell's enums, and equivalent to Ada's variant record types) are different from classical enums in other languages, because the enum variants can have fields of their own.

I suggest you change your code to

#[derive(Debug, Serialize, Deserialize)]
enum Contract {
    Bar { data: Vec<Foo> },
    Baz { data: Vec<Foo>, tag: String },
}

#[derive(Debug, Serialize, Deserialize)]
struct Foo {
    x: u32,
    y: u32,
}

impl Contract {
    fn do_something(&self) {
        match *self {
            Contract::Bar { ref data } => println!("I'm a Bar and this is my data {:?}", data),
            Contract::Baz { ref data, ref tag } => {
                println!("I'm Baz {} and this is my data {:?}", tag, data)
            }
        }
    }
}
oli_obk
  • 28,729
  • 6
  • 82
  • 98
8

You can use typetag to solve the problem. Add #[typetag::serde] (or ::deserialize, as shown here) to the trait and each implementation:

use serde::Deserialize;

#[typetag::deserialize(tag = "driver")]
trait Contract {
    fn do_something(&self);
}

#[derive(Debug, Deserialize, PartialEq)]
struct File {
    path: String,
}

#[typetag::deserialize(name = "file")]
impl Contract for File {
    fn do_something(&self) {
        eprintln!("I'm a File {}", self.path);
    }
}

#[derive(Debug, Deserialize, PartialEq)]
struct Http {
    port: u16,
    endpoint: String,
}

#[typetag::deserialize(name = "http")]
impl Contract for Http {
    fn do_something(&self) {
        eprintln!("I'm an Http {}:{}", self.endpoint, self.port);
    }
}

fn main() {
    let f = r#"
{
  "driver": "file",
  "path": "/var/log/foo"
}
"#;

    let h = r#"
{
  "driver": "http",
  "port": 8080,
  "endpoint": "/api/bar"
}
"#;

    let f: Box<dyn Contract> = serde_json::from_str(f).unwrap();
    f.do_something();

    let h: Box<dyn Contract> = serde_json::from_str(h).unwrap();
    h.do_something();
}
[dependencies]
serde_json = "1.0.57"
serde = { version = "1.0.114", features = ["derive"] }
typetag = "0.1.5"

See also:

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
  • Thanks for your answer, Shep! Was this inspired by the answer I just received to another of my questions? https://stackoverflow.com/questions/57560593/why-do-i-get-an-unsupportedtype-error-when-serializing-to-toml-with-a-manually-i/63272188#63272188 – Dash83 Aug 06 '20 at 08:41
  • 1
    @Dash83 it was actually due to [How can I use serde to deserialize into a hierarchical decentralized configuration?](https://stackoverflow.com/q/63253424/155423), which I wanted to close as a duplicate of this and [How can deserialization of polymorphic trait objects be added in Rust if at all?](https://stackoverflow.com/q/44231020/155423) (which I'm sad that both questions exist to start with). Neither question had an answer showing how to use typetag though. – Shepmaster Aug 06 '20 at 12:41
3

Adding on to oli_obk's answer, you can use Serde's enum representation to distinguish between the types.

Here, I use the internally-tagged representation to deserialize these two similar objects into the appropriate variant:

{
  "driver": "file",
  "path": "/var/log/foo"
}
{
  "driver": "http",
  "port": 8080,
  "endpoint": "/api/bar"
}
use serde; // 1.0.82
use serde_derive::*; // 1.0.82
use serde_json; // 1.0.33

#[derive(Debug, Deserialize, PartialEq)]
#[serde(tag = "driver")]
enum Driver {
    #[serde(rename = "file")]
    File { path: String },
    #[serde(rename = "http")]
    Http { port: u16, endpoint: String }
}

fn main() {
    let f = r#"   
{
  "driver": "file",
  "path": "/var/log/foo"
}
"#;

    let h = r#"
{
  "driver": "http",
  "port": 8080,
  "endpoint": "/api/bar"
}
"#;

    let f: Driver = serde_json::from_str(f).unwrap();
    assert_eq!(f, Driver::File { path: "/var/log/foo".into() });

    let h: Driver = serde_json::from_str(h).unwrap();
    assert_eq!(h, Driver::Http { port: 8080, endpoint: "/api/bar".into() });
}

You don't have to squash it all into one enum, you can create separate types as well:

#[derive(Debug, Deserialize, PartialEq)]
#[serde(tag = "driver")]
enum Driver {
    #[serde(rename = "file")]
    File(File),
    #[serde(rename = "http")]
    Http(Http),
}

#[derive(Debug, Deserialize, PartialEq)]
struct File {
    path: String,
}

#[derive(Debug, Deserialize, PartialEq)]
struct Http {
    port: u16,
    endpoint: String,
}
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
  • how would you 'unpack' the struct from the second example? at the moment the output would be `File(File{path:"foo"})`. What's the cleanest implementation to get `File{path:"foo"}` instead? – Marcin Mar 05 '21 at 03:35