#[derive(Serialize, Deserialize, Debug)]
enum Motion {
Column(i32),
Line(i32),
}
#[derive(Serialize, Deserialize, Debug)]
enum Command {
Move(Motion),
Delete(Motion),
Input(String),
}
fn main() {
let cmd = Command::Move(Motion::Column(1));
let cmd_str = serde_json::to_string(&cmd).unwrap();
println!("result: {}", cmd_str);
}
this will get result like this:
{
"Move": {
"Column": 1
}
}
Is it possible to make it be:
{
"type": "Move",
"content": {
"type": "Column",
"content": 1
}
}
Here is one approach. Manually implement Serialize for a Wrapper<T> type that contains "type" and "content".
#[derive(Serialize)]
struct Wrapper<T> {
#[serde(rename = "type")]
name: String,
content: T,
}
impl<T> Wrapper<T> {
fn new(name: &'static str, content: T) -> Self {
Wrapper {
name: name.to_string(),
content: content,
}
}
}
impl serde::Serialize for Motion {
fn serialize<S>(&self, ser: &mut S) -> Result<(), S::Error>
where S: serde::Serializer
{
match *self {
Motion::Column(ref i) => Wrapper::new("Column", i).serialize(ser),
Motion::Line(ref i) => Wrapper::new("Line", i).serialize(ser),
}
}
}
impl serde::Serialize for Command {
fn serialize<S>(&self, ser: &mut S) -> Result<(), S::Error>
where S: serde::Serializer
{
match *self {
Command::Move(ref m) => Wrapper::new("Move", m).serialize(ser),
Command::Delete(ref m) => Wrapper::new("Delete", m).serialize(ser),
Command::Input(ref s) => Wrapper::new("Input", s).serialize(ser),
}
}
}
You could use a macro to make this a lot nicer so it becomes just something like:
tagged_enum!(Motion {
Column(i32),
Line(i32),
});
related discussion: https://github.com/serde-rs/json/issues/67
Here is a complete working solution. For deserialization it uses a serde_json::Value to partially deserialize the input, check the "type", then deserialize the "content" into the correct Rust type. It has readable error messages when things go wrong, for example if the input is {"type":"Bla","content":1} you get "unrecognized Command type: Bla" and if the input is {"type":"Move","content":[0]} you get "bad Move content: [0]".
#![feature(plugin, custom_derive)]
#![plugin(serde_macros)]
extern crate serde;
extern crate serde_json as json;
////////////////////////////////////////////////////////////////////////////////
#[derive(Serialize, Deserialize)]
struct Wrapper {
#[serde(rename = "type")]
name: String,
content: json::Value,
}
impl Wrapper {
fn new<T>(name: &'static str, content: T) -> Self
where T: serde::Serialize
{
Wrapper {
name: name.to_string(),
content: json::to_value(&content),
}
}
}
macro_rules! tagged_enum {
($name:ident { $( $variant:ident ( $content:ty ), )* }) => {
#[derive(Debug)]
enum $name {
$(
$variant($content),
)*
}
impl ::serde::Serialize for $name {
fn serialize<S>(&self, ser: &mut S) -> Result<(), S::Error>
where S: ::serde::Serializer
{
match *self {
$(
$name::$variant(ref content) => {
Wrapper::new(stringify!($variant), content)
}
)*
}.serialize(ser)
}
}
impl ::serde::Deserialize for $name {
fn deserialize<D>(des: &mut D) -> Result<Self, D::Error>
where D: ::serde::Deserializer,
{
use ::serde::Error;
let wrapper = try!(Wrapper::deserialize(des));
let str_for_error = wrapper.content.to_string();
match &wrapper.name[..] {
$(
stringify!($variant) => Ok($name::$variant(try!(
json::from_value(wrapper.content)
.map_err(|_| D::Error::custom(format!(
"bad {} content: {}", stringify!($variant), str_for_error)))
))),
)*
other => {
Err(D::Error::custom(format!(
"unrecognized {} type: {}", stringify!($name), other)))
}
}
}
}
}
}
////////////////////////////////////////////////////////////////////////////////
tagged_enum!(Motion {
Column(i32),
Line(i32),
});
tagged_enum!(Command {
Move(Motion),
Delete(Motion),
Input(String),
});
fn main() {
let cmd = Command::Move(Motion::Column(1));
let cmd_str = json::to_string_pretty(&cmd).unwrap();
println!("serialized command - {}", cmd_str);
let cmd: Command = json::from_str(&cmd_str).unwrap();
println!("deserialized command - {:?}", cmd);
}
thank you all, @dtolnay your solution is great.
Any plans of having something like the tagged_enum!() above (possibly with a configurable name for the typefield) exposed directly by serde or serde-json? This data layout seems common enough, at least in JSON world.
Yes we need to provide some way to make this easier. https://github.com/serde-rs/json/issues/67 is tracking a similar request.
I have a solution in #739 that looks like:
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "type")]
enum Motion {
Column { content: i32 },
Line { content: i32 },
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "type")]
enum Command {
Move { content: Motion },
Delete { content: Motion },
Input { content: String },
}
Let me know if anyone has feedback. This case is a bit repetitive because all of the variants have the same content field, but in general you could have an enum where each variant has different number of fields or names of fields.
I would be open to extending this later to something like this if people think this is a common pattern:
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "type", content = "content")]
enum Motion {
Column(i32),
Line(i32),
}
@dtolnay Awesome work. I am very expected to see it merged!
@hh9527 the even better representation is available in Serde 0.9.8.
#[derive(Serialize, Deserialize)]
#[serde(tag = "type", content = "content")]
enum Motion {
Column(i32),
Line(i32),
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "type", content = "content")]
enum Command {
Move(Motion),
Delete(Motion),
Input(String),
}
@dtolnay very neat! great job!
Most helpful comment
Here is a complete working solution. For deserialization it uses a
serde_json::Valueto partially deserialize the input, check the "type", then deserialize the "content" into the correct Rust type. It has readable error messages when things go wrong, for example if the input is{"type":"Bla","content":1}you get "unrecognized Command type: Bla" and if the input is{"type":"Move","content":[0]}you get "bad Move content: [0]".