data.jsonでJSONの読み書き
(このエントリは Clojure Contrib Library Advent Calendar 3日目の記事です。)
はじめに
3日目の今日はdata.jsonについてです。
data.jsonは名前が示すとおり,JSON形式の読み書きをするために使います。以前はclojure.contrib.jsonという名前でした。
ひと昔前は,ClojureでJSONというと,clojure-jsonやclj-json,cheshireなどいろいろなライブラリが競合していました。現在では,data.jsonがデファクトスタンダード的な位置にあり,Web関連のアプリケーションを中心に広く使われています。初日の記事の Clojure Contrib ライブラリとは に記載したランキングを見ても,data.jsonがよく使われていることが分かります。
このエントリでは,data.jsonの使い方を説明し,その後で実用例としてGitHub APIからリポジトリの情報を取得する例を見てみます。
インストール
Leiningenを使用している場合は,project.cljの:dependenciesに
[org.clojure/data.json "0.2.3"]
と追加するだけで使えます。2013年12月現在では,0.2.3が最新の安定版です。
以下はproject.cljのサンプルです。
(defproject test-json "0.1.0-SNAPSHOT" :description "test project for data.json" :url "http://example.com/test-json" :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html"} :dependencies [[org.clojure/clojure "1.5.1"] [org.clojure/data.json "0.2.3"]])
使い方
data.jsonは非常にシンプルなライブラリで,提供する関数は以下の5つのみです*1。
- read:ReaderからJSONデータをClojureのオブジェクトとして読み込む
- read-str:文字列からJSONデータを読み込む
- write:WriterにClojureのオブジェクトをJSONとして書き出す
- write-str:文字列に書き出す
- pprint:JSONをプリティプリントした結果を標準出力(*out*)に書き出す
なお,これらの関数を使う場合には,名前空間clojure.data.jsonをrequireする必要があります。
(require '[clojure.data.json :as json])
ここでは,readとwriteについて詳しく説明します。その他の関数について,詳細は公式ドキュメントを参照して下さい。
(read reader & options)
readはReader(java.io.Reader)と0個以上のオプションを引数にとります。提供されているオプションは次の5つです:
- :eof-error?:trueを指定すると,EOFを読み込んだ場合に例外を投げます(デフォルトはtrue)
- :eof-value::eof-error?がfalseでEOFを読み込んだ場合に返す値を指定します(デフォルトはnil)
- :bigdec:trueを指定すると,小数値を読み込んだ場合に(Doubleでなく)BigDecimalに変換します
- :key-fn:オブジェクトのキーを変換する際に使用する関数を指定します
- :value-fn:オブジェクトの値を変換する際に使用する関数を指定します
以下は入力例です*2:
; :eof-error?を指定しないでEOFを読み込んだ場合は例外が投げられます user=> (with-in-str "" (json/read *in*)) EOFException JSON error (end-of-file) clojure.data.json/-read (json.clj:180) ; :eof-error?にfalseを指定すると,EOFを読み込んだ場合にデフォルトではnilが返ります user=> (with-in-str "" (json/read *in* :eof-error? false)) nil ; :eof-valueでEOFを読み込んだ場合に返す値を指定できます user=> (with-in-str "" (json/read *in* :eof-error? false :eof-value 'nothing)) nothing ; :bigdecを指定しないで小数値を読み込んだ場合はDoubleに変換されます user=> (with-in-str "1.0" (json/read *in*)) 1.0 user=> (class *1) java.lang.Double ; :bigdecにtrueを指定すると,小数値を読み込んだ場合にBigDecimalに変換されます user=> (with-in-str "1.0" (json/read *in* :bigdec true)) 1.0M user=> (class *1) java.math.BigDecimal ; :key-fnを指定しないでオブジェクトを読み込んだ場合,オブジェクトのキーはそのまま文字列になります user=> (with-in-str "{\"a\" 1, \"b\" 2}" (json/read *in*)) {"a" 1, "b" 2} ; :key-fnにkeyword関数を指定すると,オブジェクトのキーはキーワードに変換されます user=> (with-in-str "{\"a\" 1, \"b\" 2}" (json/read *in* :key-fn keyword)) {:a 1, :b 2} ; もちろんkeyword関数以外も指定できます user=> (with-in-str "{\"a\" 1, \"b\" 2}" (json/read *in* :key-fn symbol)) {a 1, b 2} ; :value-fnに関数を指定すると,その関数がオブジェクトの値を変換するのに使用されます ; :value-fnに指定する関数は2引数関数で,第1引数としてキー,第2引数として値がそれぞれ渡されます user=> (with-in-str "{\"a\" 1, \"b\" 2}" (json/read *in* :value-fn (fn [k v] [k v]))) {"a" ["a" 1], "b" ["b" 2]} user=>
(write x writer & options)
writeはJSONとして書き出すオブジェクトとWriter(java.io.Writer),0個以上のオプションを引数にとります。提供されているオプションは次の5つです:
- :escape-unicode:trueを指定すると,非ASCII文字を\uXXXXという表記へエスケープします(デフォルトはtrue)
- :escape-js-separators:trueを指定すると,U+2028およびU+2029というユニコード文字を(:escape-unicodeがfalseの場合でも)\u2028および\u2029へエスケープします(デフォルトはtrue)
- :escape-slash:trueを指定すると,スラッシュ(/)を\/へエスケープします
- :key-fn:オブジェクトのキーを変換する際に使用する関数を指定します
- :value-fn:オブジェクトの値を変換する際に使用する関数を指定します
以下は入力例です*3:
; :escape-unicodeを指定しないと,非ASCII文字が\uXXXXへエスケープされます user=> (with-out-str (json/write {"question" "進捗どうですか?", "answer" "進捗ダメです"} *out*)) "{\"question\":\"\\u9032\\u6357\\u3069\\u3046\\u3067\\u3059\\u304b\\uff1f\",\"answer\":\"\\u9032\\u6357\\u30c0\\u30e1\\u3067\\u3059\"}" ; :escape-unicodeにfalseを指定すると,非ASCII文字もエスケープされずそのまま出力されます user=> (with-out-str (json/write {"question" "進捗どうですか?", "answer" "進捗ダメです"} *out* :escape-unicode false)) "{\"question\":\"進捗どうですか?\",\"answer\":\"進捗ダメです\"}" ; writeに渡すオブジェクトのキーは,:key-fnに渡す関数によって変換することができます user=> (with-out-str (json/write {:a 1, :b 2} *out* :key-fn name)) "{\"a\":1,\"b\":2}" ; ただし,オブジェクトのキーがキーワードやシンボルの場合は自動的に文字列に変換されるので,:key-fnを指定する必要はありません user=> (with-out-str (json/write {:a 1, :b 2} *out*)) "{\"a\":1,\"b\":2}" user=> (with-out-str (json/write '{a 1, b 2} *out*)) "{\"a\":1,\"b\":2}" ; :value-fnでオブジェクトの値を変換することもできます user=> (with-out-str (json/write {:a 1, :b 2} *out* :value-fn (fn [k v] [k v]))) "{\"a\":[\"a\",1],\"b\":[\"b\",2]}" user=>
例
さて,それではdata.jsonの実用的な例として,GitHub APIからリポジトリの情報を取得する例を見てみます。
GitHub APIは,レスポンスがJSON形式で返ってくるので,readでClojureのマップとして読み込んだ後に,必要なキーだけを選択するようにします。
(ns contrib-calendar.data.json (:require [clojure.data.json :as json] [clojure.java.io :as io]) (:import java.net.URL)) (def ^:const ^:private %github-api-base-uri "https://api.github.com/repos") (defn retrieve-repo-info [owner repo] (let [url (URL. (str %github-api-base-uri "/" owner "/" repo))] (with-open [r (io/reader (.openStream url))] (-> (json/read r :key-fn keyword) (select-keys [:size :stargazers_count :watchers_count])))))
以下の実行例では,ClojureのGitHubリポジトリ(https://github.com/clojure/clojure)の情報を取得しています。
user=> (use 'contrib-calendar.data.json) nil user=> (retrieve-repo-info "clojure" "clojure") {:watchers_count 2670, :stargazers_count 2670, :size 10499} user=>