Serialization in Rust

One of my first projects with Rust is the spaceapi implementation of my local hackerspace.

You can find the source for it on github.

The Space API

The Space API is a rather simple JSON API used to expose information about a hackerspace. It consist of some static data like the location or some contact information and some dynamic data like the current state of the space.

Implementing JSON serialization in Rust is easy when using the rustc_serialize crate.

Using the #[derive(RustcEncodable)] attribute on a type, serializing a struct is as easy as calling json::encode(&status).unwrap().

extern crate rustc_serialize;
use rustc_serialize::json;

#[derive(RustcEncodable)]
pub struct Status {
    pub api: String,
    pub space: String,
    pub url: String,
}

fn main() {
    let status: String = json::encode(&Status{
        api: "0.13".to_string(),
        space: "coredump".to_string(),
        url: "https://www.coredump.ch/".to_string(),
    }).unwrap();
    println!("{}", status);
}

This code will output:

{"api":"0.13","space":"coredump","url":"https://www.coredump.ch/"}

As you can see, the field names map directly to keys in the JSON output. That way we can model the Space API objects with structs and have a simple typesafe solution!

The 'type' property

Unfortunately parts of the Space API contain the property type which is a keyword in Rust. Of course using it as an identifier for a Struct field yields a compile error:

#[derive(RustcEncodable)]
pub struct Feed {
    pub type: String,
    pub url: String,
}
main.rs:7:9: 7:13 error: expected identifier, found keyword `type`
main.rs:7     pub type: String,
                  ^~~~

So we need to rename the field to something like _type and change the serialization.

Implementing ToJson

My first idea was to implement the ToJson trait for Feed. Implementing ToJson is easy, there is just one method to_json which returns the Json object:

impl json::ToJson for Feed {
    fn to_json(&self) -> Json {
        let mut feed = BTreeMap::new();
        feed.insert("type".to_string(), self._type.to_json());
        feed.insert("url".to_string(), self.url.to_json());
        Json::Object(feed)
    }
}

Sadly this means the Feeds struct can't hold the Feed struct directly anymore, but needs it as a Json object:

#[derive(RustcEncodable)]
pub struct Feeds {
    pub blog: Json,
}

Otherwise the Encodable trait can't be derived anymore, because it's not implemented for the type Feed. Since this reduces type safety somewhat (we could set blog field to any Json string), I discarded this solution.

Implementing Encodable

To regain type safety we need to implement the Encodable trait for Feed. The required method for Encodable may be a bit intimidating at the first look:

pub trait Encodable {
    fn encode<S: Encoder>(&self, s: &mut S) -> Result<(), S>;
}

Basically it takes a reference to self and an Encoder and returns an Result containing either OK() on success or Err(S) on failure. But how does one use the encoder?

Using the generated code as base

After some googleing I found out that one can show the macro expanded source code. It's kinda like the pre-processor output when coming from the C/C++ world.

rustc -Z unstable-options src/spaceapi.rs --pretty expanded

The expanded code is pretty readable and it was easy to find the corresponding trait implementation generated by the Rust compiler:

impl ::rustc_serialize::Encodable for Feed {
    fn encode<__S: ::rustc_serialize::Encoder>(&self, __arg_0: &mut __S)
     -> ::std::result::Result<(), __S::Error> {
        match * self {
            Feed { _type: ref __self_0_0, url: ref __self_0_1 } =>
            __arg_0.emit_struct("Feed", 2usize, |_e| -> _ {
                                match _e.emit_struct_field("_type", 0usize,
                                                           |_e| -> _ {
                                                           (* __self_0_0).encode(_e)
                                                       }) {
                                    ::std::result::Result::Ok(__try_var) =>
                                    __try_var,
                                    ::std::result::Result::Err(__try_var) =>
                                    return ::std::result::Result::Err(__try_var),
                                };
                                return _e.emit_struct_field("url", 1usize,
                                                            |_e| -> _ {
                                                            (* __self_0_1).encode(_e)
                                                        }); }),
        }
    }
}

So from there I tried to make the code a bit more readable which resulted in the following:

impl Encodable for Feed {
    fn encode<S: Encoder>(&self, encoder: &mut S) -> Result<(), S::Error> {
        match * self {
            Feed { _type: ref p_type, url: ref p_url } =>
                encoder.emit_struct("Feed", 2usize, |enc| -> _ {
                    try!(enc.emit_struct_field( "type", 0usize, |enc| p_type.encode(enc)));
                    return enc.emit_struct_field("url", 1usize, |enc| -> _ { (* p_url).encode(enc) });
                }),
        }
    }
}

The final encoded String looks like this:

{"type":"rss","url":"https://www.coredump.ch/feed/"}

Have comments? Discuss on Hacker News.

blogroll

social