ADVENT CALENDAR 2019

【Rust】serde_jsonの使い方

By Kaisei Yokoyama

Amusement Creators 2019 アドベントカレンダー 2日目(122)の記事です。Rustのserde_jsonを使って, AmCrのメンバーのデータを読み取っていきます。

What is serde_json ?

serde.rs

プログラミング言語Rustにおいて、Json形式 のデータを楽に読み込んだり、吐き出したりするのにとても便利なcrate(クレート: Rustのライブラリのこと)です。

ちなみに、読み込むことをDeserialize, 吐き出すことをSerializeといいます。

Use easily

use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let point = Point { x: 1, y: 2 };
// selialize: インスタンス->文字列
let serialized = serde_json::to_string(&point).unwrap();
println!("serialized = {}", serialized);
// deselialize: 文字列->インスタンス
let deserialized: Point = serde_json::from_str(&serialized).unwrap();
println!("deserialized = {:?}", deserialized);
}
view raw playground.rs hosted with ❤ by GitHub

Point構造体のderiveに着目してください。それぞれ、

derive 意味
Serialize インスタンス->Jsonへの変換を実装する
Deserialize Json->インスタンスへの変換を実装する

という効果があります。

Detail

serde_jsonの機能をもっと詳しく見ていきましょう。

ここでは、members.jsonを読み込むことを目標にします。

serde_jsonを使うには、cargo.tomlに次のように設定してください。

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

ひな形

wikiによれば、ここで使われているjsonオブジェクトの形式は、Rustのデータ型を用いて次のように表すことができます。

use serde::{Serialize, Deserialize};
/// members.json の内容すべて
#[derive(Serialize,Deserialize,Debug)]
pub struct Members(Vec<Cluster>);
/// 入部年度ごとのメンバーの集合
#[derive(Serialize,Deserialize,Debug)]
pub enum Cluster {
/// 現役
Active {
year: u32,
members: Vec<Member>,
},
/// 卒業生
Alumni {
title: String,
members: Vec<Member>,
},
}
#[derive(Serialize,Deserialize,Debug)]
pub struct Member {
name: String,
description: String,
thumbnail: Option<String>,
tags: Option<Vec<String>>,
sns: Option<SNS>,
}
#[derive(Serialize,Deserialize,Debug)]
pub struct SNS {
twitter: Option<String>,
github: Option<String>,
link: Option<Link>,
}
#[derive(Serialize,Deserialize,Debug)]
pub struct Link {
url: String,
title: String,
}
view raw members.rs hosted with ❤ by GitHub

必須ではないパラメーターを表すには、Option<T>を使います。

改善

パラメーターのリネーム

現在のままでは、この構造体はパラメーターがスネークケース(snake_case)で書かれたものにしか対応しません1。パラメーターをリネームして、パスカルケース(PascalCase)に対応させる必要があります。

といっても、Rustの方針に逆らって構造体のフィールドをリネームする必要はありません。serde_jsonには、このような場合にリネームを挟んでくれる機能があります。

#[serde(rename_all = "PascalCase")]

です。この表記(アトリビュートといいます)を、適用するstructおよびenumのVariantにつけましょう。

また、表記規則だけでなく、直接パラメーターの名前を変更することもできます。members.jsonの中には一箇所だけ適切なパスカルケースにマッチしない部分がありますので(どうしてそんな仕様にした)そこを訂正しましょう。

#[derive(Serialize,Deserialize,Debug)]
#[serde(rename_all = "PascalCase")]
pub struct Member {
    name: String,
    description: String,
    thumbnail: Option<String>,
    tags: Option<Vec<String>>,
    #[serde(rename="SNS")]
    sns: Option<SNS>,
}

enumのシリアライズ

もうひとつ、問題があります。Cluster列挙体です。

serde_jsonのデフォルトの挙動では、Jsonオブジェクトに該当する列挙体のバリアントの名前を書かなくてはなりません。例えば、

let amcr_2018 = Cluster::Active {
        year: 2018,
        members: Vec::new(),
    };

は、

  {"Active":{"Year":2018,"Members":[]}}

に対応します(playground)。

これを、

  {"Year":2018,"Members":[]}

に対応させるためには、

#[serde(untagged)]

というアトリビュートを列挙体に対して使用します(playground)。

これで必要なデータ型は完成です。

Jsonファイルから読み込む

読み取った文字列をserde_json::from_str()に代入するという方法もありますが、ファイルから直接インスタンス化する方法もあります。

serde_json::from_reader()を使います。

let members_json = std::fs::File::open("members.json")?;
let members : members::Members = serde_json::from_reader(members_json)?;

完成

お疲れさまでした。これで、ローカルのmembers.json2を読み取ることのできるRustのプログラムが完成です。

途中でわからなくなった人もご心配なく。今回の記事のリポジトリを用意しておきました。serde_usage

Advanced

せっかくなので、ローカルのmembers.jsonではなく、github上のものを読み込むようにしてみましょう。reqwestクレートを使います。

fn main() -> Result<(), Box<dyn Error>> {
use members::*;
let members: Members
= reqwest::get("https://raw.githubusercontent.com/AmusementCreators/WebSite/master/data/members.json")?
.json()?;
let Members(members) = members;
struct Sum {
active: usize,
alumni: usize,
}
/// Sumに足し算を定義する
impl Add for Sum {
type Output = Sum;
fn add(self, rhs: Self) -> Self::Output {
Self {
active: self.active + rhs.active,
alumni: self.alumni + rhs.alumni,
}
}
}
/// 総和の計算方法を定義する
impl std::iter::Sum for Sum {
fn sum<I: Iterator<Item=Sum>>(iter: I) -> Self {
iter.fold(Sum { active: 0, alumni: 0 }, |sum, i| sum + i)
}
}
let Sum { active, alumni } = members.iter()
// それぞれのクラスタから数値(人数)を読み取る
.map(|c| match c {
Cluster::Active { members, .. } => Sum { active: members.len(), alumni: 0 },
Cluster::Alumni { members, .. } => Sum { active: 0, alumni: members.len() }
})
// 総和を取る
.sum();
println!("Active: {}, Alumni: {}", active, alumni);
Ok(())
}
view raw main.rs hosted with ❤ by GitHub

実行

$ cargo run
   Compiling serde_usage v0.1.0 (/Users/hoge/github/serde_usage)
    Finished dev [unoptimized + debuginfo] target(s) in 8.79s
     Running `target/debug/serde_usage`
Active: 23, Alumni: 8

自己紹介が掲載されているのは、現役23人・卒業生8人のようです3

こちらもリポジトリを用意しておきました。Rustの実行環境があれば、git cloneしてcargo runで実行できます。serde_usage/advanced

おわり

serde_jsonには、紹介したものの他にも様々な機能があります。examplesを読んでいるだけでも結構面白いので、みなさんも一度は目を通して、必要な機能を把握しておくとよいかもしれません。


  1. 変数名がスネークケースで書かれているため [return]
  2. 2019/11/14日現在 [return]
  3. 2019/11/15日現在 [return]

SHARE THIS POST