data.jsonでJSONの読み書き

(このエントリは Clojure Contrib Library Advent Calendar 3日目の記事です。)

はじめに

3日目の今日はdata.jsonについてです。
data.jsonは名前が示すとおり,JSON形式の読み書きをするために使います。以前はclojure.contrib.jsonという名前でした。

ひと昔前は,ClojureでJSONというと,clojure-jsonclj-jsoncheshireなどいろいろなライブラリが競合していました。現在では,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=> 

おわりに

今回は,data.jsonの使い方と実用例としてGitHub APIからリポジトリの情報を取得する例を見ました。

次回は@halcat0x15aさんのcore.asyncについての記事です。Don't miss it!

*1:その他,過去のバージョンとの互換性のためにjson-*,*-jsonといった名前のつけられた関数が提供されていますが,いずれもdeprecatedな扱いになっています

*2:この例のように,単に文字列からJSONを読み込みたい場合にはread-strを使用した方が便利です

*3:readの場合と同様,この例のように,単にJSONを文字列へ書出したい場合にはwrite-strを使用した方が便利です