3

I am trying to use serde to serialize trait objects. I had a look at

but I do not understand how to put the pieces together. Please show me how to proceed with this.


This code mimics the example in The Rust Programming Language:

use serde::Serialize; // 1.0.126

trait Paint: Serialize {
    fn paint(&self) -> String;
}

#[derive(Serialize)]
struct Pen {
    color: String,
}

#[derive(Serialize)]
struct Brush {
    color: String,
}

impl Paint for Pen {
    fn paint(&self) -> String {
        return format!("Pen painting with color {}", self.color);
    }
}

impl Paint for Brush {
    fn paint(&self) -> String {
        return format!("Brush painting with color {}", self.color);
    }
}

#[derive(Serialize)]
struct Canvas {
    height: f32,
    width: f32,
    tools: Vec<Box<dyn Paint>>,
}

impl Paint for Canvas {
    fn paint(&self) -> String {
        let mut s = String::new();
        for tool in &self.tools {
            s.push_str("\n");
            s.push_str(&tool.paint());
        }

        return s;
    }
}

fn main() {
    let pen = Pen {
        color: "red".to_owned(),
    };
    let brush = Brush {
        color: "blue".to_owned(),
    };
    let canvas = Canvas {
        height: 12.0,
        width: 10.0,
        tools: vec![Box::new(pen), Box::new(brush)],
    };

    println!("{}", canvas.paint());
    serde_json::to_string(&canvas).unwrap();
}

The code does not compile due to the object-safety rules:

error[E0038]: the trait `Paint` cannot be made into an object
   --> src/main.rs:33:12
    |
33  |     tools: Vec<Box<dyn Paint>>,
    |            ^^^^^^^^^^^^^^^^^^^ `Paint` cannot be made into an object
    |
    = help: consider moving `serialize` to another trait
note: for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit <https://doc.rust-lang.org/reference/items/traits.html#object-safety>
   --> /playground/.cargo/registry/src/github.com-1ecc6299db9ec823/serde-1.0.126/src/ser/mod.rs:247:8
    |
247 |     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    |        ^^^^^^^^^ ...because method `serialize` has generic type parameters
    | 
   ::: src/main.rs:3:7
    |
3   | trait Paint: Serialize {
    |       ----- this trait cannot be made into an object...

I understand that trait objects cannot have methods with generic parameters, but in my case I only need to serialize the Canvas struct to JSON. Is there something I could do to make that work?

The ideal serialized output would be

{
    "Canvas": {
        "height": 12.0,
        "width": 10.0,
        "tools": {
            "Pen": {
                "color": "red"
            },
            "Brush": {
                "color": "blue"
            }
        }
    }
}
fvall
  • 380
  • 2
  • 9
  • Your "ideal" output [_isn't valid JSON_](https://jsonlint.com/). – Shepmaster May 25 '21 at 20:26
  • edited ideal output – fvall May 25 '21 at 20:54
  • [This](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=748b031513dcfc3462f705ac611e76b8) compiles (though not on the playground which doesn't include `erased_serde`) and runs, but it still doesn't provide precisely your ideal output, it's lacking the type tags. It probably requires a bit more work to get there, but I think it's a good starting point. Good luck! – user4815162342 May 25 '21 at 21:17
  • I think you could adapt it to use an enum wrapping the Pen and Brush objects instead of the `dyn Paint` – Netwave May 26 '21 at 09:06
  • @Netwave The OP is likely aware of that, but the `dyn Paint` approach is much more general because it allows an external user of the API to create a new `Paint` implementation which the upstream is unaware of. – user4815162342 May 26 '21 at 10:25
  • Thanks @user4815162342. I tried your code and it compiles. I think I can patch your solution with https://serde.rs/container-attrs.html#tag, which should be a hacky but sufficient solution to my problem. – fvall May 26 '21 at 11:06
  • Thanks @Netwave, I will explore that idea. Thinking about it now, it may be better to restrict the flexibility to a fixed number of types (via the enum) instead of potentially having any custom implementation of the trait. I still have not made my mind what actually would be best for my use case. – fvall May 26 '21 at 11:10
  • If you can use an enum, that's almost certainly what you should go for because it will make your life easier in a number of ways, not just with serde. – user4815162342 May 26 '21 at 11:12
  • Trying to imagine how this would work. Deserialization would have to look at the serialized data and get some kind of type specifier ("Brush" or "Pen" or whatever), then use that to find a Deserialize impl for the specified type. But Rust doesn't really have that capability (dynamically finding an impl by type), which explains why this isn't easy... – Jason Orendorff Aug 29 '22 at 16:50

0 Answers0