Edited at
RustDay 12

RustのJSONシリアライザをいろいろと試してみる(+α)

More than 1 year has passed since last update.

この記事は、Rustアドベントカレンダー 2015 12日目の記事です。

この記事では、様々なRustのJSONパーサとシリアライザを試してみた結果をご紹介しようと思います。


忙しい人のための今おすすめのライブラリ

serde-rs/json です。serde-rs/json を使いましょう!


ここから本題


serde-rs/json

serdeはRustの強力なシリアライズ用のフレームワークです。stableで安定しています。


Usage

extern crate serde;

extern crate serde_json;
use std::collections::BTreeMap;
fn main() {
let s = r#"{"x":1,"y":2}"#;
let deserialized_map: BTreeMap<String, f64> = serde_json::from_str(s).unwrap();
println!("{:?}", deserialized_map);
// => {"x": 1, "y": 2}
println!("{:?}", deserialized_map.get("x").unwrap());
// => 1
}

シンプルに使うことが出来ます。しかし、予め型を指定しなければいけないので異なる型だと途端にエラーになります。

extern crate serde;

extern crate serde_json;
use std::collections::BTreeMap;
fn main() {
extern crate serde;
extern crate serde_json;
use std::collections::BTreeMap;
let s = r#"{"x":1,"y": [1,2,3,4] }"#;
// let deserialized_map: BTreeMap<String, f64> = serde_json::from_str(s).unwrap();
// println!("{:?}", deserialized_map);
// => An unknown error occurred
}

serde_macros を使用することが

extern crate serde;

extern crate serde_json;
use std::collections::BTreeMap;
fn main() {
/* #![feature(plugin)] */
/* #![plugin(serde_macros)] */
// => serde_macros の コンパイルエラーが発生(stable releaseだと使えないらしい)
}


jsonway

DSLで書けるJSONシリアライザです。DSLなので、元のオブジェクトを変換するというわけではなさそうですね。


Usage

extern crate jsonway;

fn main() {
let json = jsonway::object(|json| {
json.set("first_name", "Luke".to_string());
json.set("last_name", "Skywalker".to_string());

json.object("info", |json| {
json.set("homeworld", "Tatooine".to_string());
json.set("born", "19 BBY".to_string());
json.set("died", "Between 45 ABY and 137 ABY".to_string());
});

json.array("masters", |json| {
json.push("Obi-Wan Kenobi".to_string());
json.push("Yoda".to_string());
json.push("Joruus C'baoth (Briefly)".to_string());
json.push("Darth Sidious (Briefly)".to_string());
});
}).unwrap();
println!("{}", json);
}

( jsonway より引用 )

とても簡潔に書くことが出来ます。


json_macros

こちらは、マクロを使ってJSON記法で書くことが出来る便利なライブラリです。しかしこちらもstable版Rustでは動きませんでした。


Usage

#![feature(plugin)] */

#![plugin(json_macros)] */
fn main() {
extern crate "rustc-serialize" as rustc_serialize;
let x = 123i32;
println!("{}", json!({ // object literal
"foo": "foooooo", // string literal keys and values
"bar": [true, null, 123, 123.4], // array, boolean, null, numeric literals
"quux": { // nest as deeply as you like
"a": [1, 2, 3, 4],
"b": { "a": null },
"c": false
},
"waldo": (192 - x) // wrap in parens to splice ToJson expressions directly
}).pretty().to_string());
}

( json_macrosより引用 )


json-rs

こちらは #[feature] を使用しており、 stable版Rustでは実行することが出来ませんでした。


rustc_serialize

今一番Rustで使われているパーサとシリアライザはこれだと思います。

指定した型に RustcDecodableRustcEncodable をderiveすると、その型のデータをシリアライズしたり、パースしたり出来ます。


Usage

extern crate rustc_serialize;

use rustc_serialize::json::{Json, Parser};

fn main () {
let mut parser = Parser::new(
r#"{
"
a": 1.0,
"
b": [
true,
"
foo\nbar",
{ "
c": {"d": null} }
]
}"
#.chars()
);

parser.next();

let foo = parser.next().unwrap();
println!("{:?}", foo);
// => F64Value(1)
}

nextで進めることによって、カーソルが変わり、取得できる値が変わる実装になっています。今回は F64Valueの型(Enumの値)を取得したのですが、中に入っているf64を取得する方法は見つけられませんでした。

通常は、予め構造体を定義し、その構造体にマップする使い方のほうが多いと思います。

extern crate rustc_serialize;

use rustc_serialize::json;
use std::collections::HashMap;

#[derive(Debug, RustcDecodable, RustcEncodable)]
struct Foo {
bar: HashMap<String, String>
}

fn main () {
let mut baz = HashMap::new();
baz.insert("test".to_owned(), "test".to_owned());
let foo = Foo { bar: baz };
let enc = json::encode(&foo).unwrap();
println!("{:?}", enc);
//=> "{\"bar\":{\"test\":\"test\"}}"
let dec: Foo = json::decode(&enc).unwrap();
println!("{:?}", dec);
//=> Foo { bar: {"test": "test"} }
}


最後に「type」 について

構造体を定義する方法でrustc_serializeを使用していたのですが、構造体では type というfieldは使用できないことに気づきました。serde ではマクロにJSONのキーと別なキー名をマップさせるマクロがあるのですが、stable版Rustでは使用できないので、辛いです。

JavaScriptの JSON.parse や Python の json.loads くらい楽なものはないかと思い、探してはみたものの見つかず、辛くなってきたので


JSONパーサを作りました

JsonFlex と言います。(crates.ioはこちら)

RustのSlackチャンネル の方にも色々と教えて頂きながらなんとか完成させることが出来ました。

またまだベータ版でテストも不十分なところがありますが、大体のJSONはパースすることが出来ます。ここで簡単なご紹介をさせていただきたいとおもいます。


Usage

use json_flex;

use json_flex::{JFObject, Unwrap};
use std::collections::HashMap;

fn main () {

let jf = json_flex::decode(r#"["a", "b", "c", ["a", "b", "c"], "d", ["ABC"],[1,2]]"#.to_owned());
let jft = format!("{:?}", jf);
assert!(jft == r#"Array([String("a"), String("b"), String("c"), Array([String("a"), String("b"), String("c")]), String("d"), Array([String("ABC")]), Array([Integer(1), Integer(2)])])"#);
println!("{}", jf.to_json());

}

このように、JavaScriptのオブジェクトのように、 foo[0][0]["baz"] といった記法で書くことが出来ます。このアプローチは、 アドベントカレンダー5日目のインデックス構文によるアクセスを実装する を参考にさせて頂きました。ありがとうございます。

JsonFlex は Enum列挙体による8個の値からなるデータの共用体です。

pub enum JFObject {

String(String),
Integer(i64),
Float(f64),
Dictionary(HashMap<String, JFObject>),
Array(Vec<JFObject>),
Null,
False,
True,
}

Null型とFalse型とTrue型は、中に要素を持たないですが、それ以外の型は中にRustの基本的な型を持ちます。それぞれ、unwrap_string等のメソッドで中身を取り出す事ができます。本当は unwrap のみで取り出したかったのですが、型がうまく推論できない場面があり、このような実装になっています。

しかし Indexを使用する場合は、十分な型推論が出来るため、foo[0][0]["baz"]と言った書き方が可能になりました。