How do I deserialize into trait, not a concrete type?
Asked Answered
O

3

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?

Overliberal answered 22/2, 2017 at 13:32 Comment(6)
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?Yulan
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.Overliberal
Are you sure you want polymorphism and not simply an enum? Do you need your code to work with user supplied types?Confidant
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.Overliberal
What about deserializing into an implementation that also implements Into for all other Contract implementations?Juggler
@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.Overliberal
C
11

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)
            }
        }
    }
}
Confidant answered 24/2, 2017 at 9:6 Comment(3)
Used the structures Bar and Baz as the associated data for the enum, but went pretty much with this design otherwise. Thanks!Overliberal
What about if there is arbitrary set of type from a trait with type parameters?Pinter
@Pinter not sure I understand. Why don't you open a new question?Confidant
S
9

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:

Shayne answered 4/8, 2020 at 19:20 Comment(2)
Thanks for your answer, Shep! Was this inspired by the answer I just received to another of my questions? #57561093Overliberal
@Overliberal it was actually due to How can I use serde to deserialize into a hierarchical decentralized configuration?, 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? (which I'm sad that both questions exist to start with). Neither question had an answer showing how to use typetag though.Shayne
S
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,
}
Shayne answered 1/1, 2019 at 17:19 Comment(1)
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?Raddle

© 2022 - 2024 — McMap. All rights reserved.