Day 6 - working with JSON
Relevancy: 1.9 stable (macros only on nightly)
JSON is a workhorse data format of the modern Web. Originating from the JavaScript world, it gained a lot of traction and at the moment it's usually the first choice of a Web developer for a data interchange format. Not only Web - once JavaScript-only, JSON support is now ubiquitous. A lot of languages ship with JSON parsers in the standard libraries, and when it's not the case, surely someone has already built a third party library. In case of Rust, JSON support comes out in the rustc_serialize::json module.
Note: in this article I deliberately do not focus on the Web, APIs, requests and so on. I mentioned JSON in a previous post about hyper but now I don't care where the JSON-encoded data comes from or what to do with it later. Here I'm just going to show you a few practical tips.
Slightly offtopic: I'm writing this blogpost while getting ready to coach a group of fantastic women at Django Girls in Łódź, Poland. If you're a woman interested in learning programming (or know such girls) check out if there's a Django Girls workshop near you! The workshops focus on basic Python, Django and web technologies, but the general idea is to get the attendees genuinely interested in programming and empowered to create.
That said, now back to JSON and Rust.
Primitives
A lot of Rust types serialize to JSON just as you would expect. Note that encode()
immutably borrows its argument:
extern crate rustc_serialize;
use rustc_serialize::Encodable;
use rustc_serialize::json::{self, Encoder};
println!("{:?}", json::encode(&42));
println!("{:?}",
json::encode(&vec!["to", "be", "or", "not", "to", "be"]));
Option<T>
maps to the encoding of value
itself if it is a Some(value)
while None
maps to null
.
Automatic (de)serialization
In the chapter on CSV I mentioned the RustcEncodable
and RustcDecodable
traits. Here's an example with a nested struct:
println!("{:?}", json::encode(&Some(true)));
let user = User {
name: "Zbyszek".to_string(),
post_count: 100u32,
likes_burgers: true,
avatar: Some(Photo {
url: "http://lorempixel.com/160/160/".to_string(),
dimensions: (160u32, 160u32),
}),
};
$ cargo run
Ok("{\"name\":\"Zbyszek\",\"post_count\":100,\"likes_burgers\":true,\"avatar\":{\"url\":\"http://lorempixel.com/160/160/\",\"dimensions\":[160,160]}}")
Pretty printing
The json::encode()
doesn't care for readability of it's output. Although the JSON it emits is correct and machine-readable, there are no newlines or indents making it hard for a human to debug. Pretty-printing is a little bit more complex than just one function call, but not too complicated:
println!("{:?}", json::encode(&user));
let mut encoded = String::new();
{
let mut encoder = Encoder::new_pretty(&mut encoded);
user.encode(&mut encoder).expect("JSON encode error");
}
Decoding
println!("{}", encoded);
let incoming_request = "{\"name\":\"John\",\"post_count\":2,\"likes_burgers\":false,\
\"avatar\":null}";
let decoded: User = json::decode(incoming_request).unwrap();
println!("My name is {} and I {} burgers",
As you cen see, decoding is also pretty easy. But what happens if we don't know all of the fields in advance? We can use another function in the json
module - from_str()
. The difference between from_str()
and decode()
is that the latter may return some struct implementing RustcDecodable
while the former returns a Json value. This type has a few methods of its own, including find()
. See the example below:
decoded.name,
if decoded.likes_burgers {
"love"
} else {
"don't like"
});
assert!(decoded.avatar.is_none());
let new_request = "{\"id\":64,\"title\":\"24days\",\"stats\":{\"pageviews\":1500}}";
We're using the if let language construct which often simplifies pattern matches where we care for only one branch and do nothing if the expression doesn't match.
The json! macro
Note: syntax extensions work only on nightly
There's one more thing I wanted to show you today. You can embed JSON-like literals directly in your Rust code with the help of the json_macros crate. This is a compiler extension that allows for some nice syntactic sugar like below:
#![feature(plugin)]
#![plugin(json_macros)]
if let Ok(request_json) = json::Json::from_str(new_request) {
if let Some(stats) = request_json.find("stats") {
if let Some(pageviews) = stats.find("pageviews") {
println!("Pageviews: {}", pageviews);
}
}
}
let config = json!({
"hostname": "localhost",
"port": 6543,
"allowed_methods": ["get", "post"],