1

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
  • 183
  • 1
  • 7
  • 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

0 Answers0