How do I deserialize into trait, not a concrete type?
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)
}
}
}
}
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,
}
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:
- How can deserialization of polymorphic trait objects be added in Rust if at all?