Data.Aeson が便利すぎる件

JSON を扱うライブラリで一番簡単に見つかるのは Text.JSON ですが、しばらく使ってると、あまりの使いにくさに発狂しそうでした。
ということで他のを探した結果、Data.Aeson というのがかなりよさげだったので、これを使うことにしました。


しばらく使ってみた感じかなり良かったので、いくつか代表的な使い方を書いてみます。

インストール

aeson ではなく、aeson-native を入れて下さい。

cabal install aeson-native

import

それぞれのコードは、これらの import (と LANGUAGE プラグマ)が先頭にあると思ってください。

{-# LANGUAGE OverloadedStrings #-}

import Control.Applicative (Applicative,pure,(<$>),(<*>))
import Control.Monad (mzero,(=<<),(<=<),(>=>))
import Data.Aeson
import Data.Aeson.Types
import Data.Either.Utils (forceEither)
import Data.Maybe (catMaybes)
import Data.Text (Text)
import qualified Data.Attoparsec as AP (Result(..),parseOnly)
import qualified Data.Attoparsec.Number as N (Number(I,D))
import qualified Data.ByteString.Lazy.Char8 as LC (unpack)
import qualified Data.Map as M (lookup,delete)

JSON データを構築する

i :: Int -> Int
i = id

s :: String -> String
s = id

value :: Value
value = 
  object ["number" .= i 10,
          "string" .= s "foo",
          "array"  .= [i 10,
                       i 20,
                       i 30],
          "object" .= object ["hoge" .= (10::Int),
                              "fuga" .= ("foo"::String)]]

main = putStrLn $ LC.unpack $ encode value

結果:(整形済み)

{"array":[10,20,30],
 "number":10,
 "object":{"fuga":"foo",
           "hoge":10},
 "string":"foo"}

i や s 関数を使うか、::Int や ::String とか書いたりしないと型がちゃんと決定できないのが面倒ですが、.= 演算子を使うことで、かなり簡単に JSON 構造を作れることが分かります。
JSON の object にデータの順序は関係無いので、キーが昇順になっていても問題ありません。
この value 関数は後の実装で使いまくります。

JSON 文字列をパースして JSON データを構築する

Value 型を JSON 文字列に変換するのには encode 関数を使います。
逆に、JSON 文字列を Value 型に変換するには Data.Attoparsec モジュールと json 関数を使います。

main = putStrLn $ LC.unpack $ encode $ forceEither
       $ AP.parseOnly json "{\"number\":10,\"object\":{\"fuga\":\"foo\",\"hoge\":10},\"string\":\"foo\"}"

結果:(整形済み)

{"number":10,
 "object":{"fuga":"foo",
           "hoge":10},
 "string":"foo"}

JSON データを自分のデータ型に入れる

さっき作った Value 型を自分の作った構造に入れてみましょう。

data MyJSON = MyJSON {
                myNumber :: Int,
                myString :: String,
                myArray :: Maybe [Int],
                myObject :: MyJSON2,
                myMaybe :: Maybe Int
              } deriving (Show,Eq)

data MyJSON2 = MyJSON2 {
                 my2Hoge :: Int,
                 my2Fuga :: String
               } deriving (Show,Eq)

instance FromJSON MyJSON where
  parseJSON (Object v) = MyJSON
                         <$> v .:  "number"
                         <*> v .:  "string"
                         <*> v .:? "array"
                         <*> v .:  "object"
                         <*> v .:? "maybe"
  parseJSON _          = mzero

instance FromJSON MyJSON2 where
  parseJSON (Object v) = MyJSON2
                         <$> v .: "hoge"
                         <*> v .: "fuga"
  parseJSON _          = mzero

parseMyJSON :: Value -> Result MyJSON
parseMyJSON = fromJSON

main = print $ parseMyJSON value

結果:(整形済み)

Success (
  MyJSON {
    myNumber = 10,
    myString = "foo",
    myArray = Just [10,20,30],
    myObject = MyJSON2 {
                 my2Hoge = 10,
                 my2Fuga = "foo"},
    myMaybe = Nothing})

Maybe 型に対して .:? 演算子を使うことで、その値のキーが存在していなくてもパース全体としては失敗しないようになります。
ここでは、"maybe" というキーは存在しませんが、全体としてはちゃんと成功していることが分かります。
もし .: 演算子を使っている場所でパースに失敗した場合は、結果が Success ではなく Error になります。

自分のデータ型を JSON データに変換する

MyJSON や MyJSON2 型を JSON データに戻してみましょう。
これは最初に使った .= 演算子を使います。

instance ToJSON MyJSON where
  toJSON v = object $ catMaybes [Just $ "number" .= myNumber v,
                                 Just $ "string" .= myString v,
                                 ("array" .=) <$> myArray v,
                                 Just $ "object" .= myObject v,
                                 ("maybe" .=) <$> myMaybe v]

instance ToJSON MyJSON2 where
  toJSON v = object ["hoge" .= my2Hoge v,
                     "fuga" .= my2Fuga v]

forceResult (Success v) = v

main = print $ LC.unpack $ encode $ toJSON $ forceResult $ parseMyJSON value

結果:(整形済み)

{"array":[10,20,30],
 "number":10,
 "object":{"fuga":"foo",
           "hoge":10},
 "string":"foo"}

最初の結果と全く同じになり、正常に元に戻すことができたことが分かります。
Maybe 型のデータが Nothing だった場合は構築してはいけないので、Nothing のデータを除けるために、リスト全体を Maybe で構築して catMaybes しています。
少し見た目が良くないので、専用の演算子とか作っておくといいかもしれません。

(.==) :: (ToJSON a, Applicative f) => Text ->   a -> f Pair
(.==) text value = pure $ text .= value

(.=?) :: (ToJSON a, Applicative f) => Text -> f a -> f Pair
(.=?) text value = (text .=) <$> value

instance ToJSON MyJSON where
  toJSON v = object $ catMaybes ["number" .== myNumber v,
                                 "string" .== myString v,
                                 "array"  .=? myArray v,
                                 "object" .== myObject v,
                                 "maybe"  .=? myMaybe v]

Value から中身を取り出す(パターンマッチ編)

普通にパターンマッチして取り出します。

get :: Text -> Value -> Maybe Value
get key (Object v) = M.lookup key v
get _   _          = Nothing

getInt (Number (N.I v)) = Just v
getInt _                = Nothing

getHoge = get "object" >=> get "hoge"

main = print $ getInt =<< getHoge value

結果:

Just "10"

パターンマッチで Object を取り出して lookup してやる関数を作ってやります。
getHoge で得られる結果は Value であるため、それを Int として扱うには、更にパターンマッチさせて取得する必要があります。結構面倒ですね。

Value から中身を取り出す(Parser 編)

parseJSON を使って取り出します。

getHoge :: Value -> Parser Int
getHoge = parseJSON >=> (.: "object") >=> (.: "hoge")

main = print $ parse getHoge value
Success 10

parseJSON で Parser 型にしてしまえば、FromJSON 型の任意の型に変換することが可能になるので、(.: "object") とすれば自動的に Object 型だと認識されてそのようにパースされ、戻り値を Parser Int とすることで、Int 型だと認識されてそのようにパースされるようになります。
FromJSON のインスタンスになってる型なら何でも変換できるのでかなり便利。

Value データの一部のデータを取り除く

Object 型は単なる Data.Map なので、Data.Map の delete で消してやるだけ。
Value → Object への変換は parseJSON を使って、Object → Value の変換は Object 型コンストラクタを使ってます。

remove :: Text -> Value -> Parser Value
remove key value = Object . M.delete key <$> parseJSON value

main = putStrLn $ LC.unpack $ encode $ forceResult
         $ parse (remove "object" <=< remove "array") value

結果:

{"number":10,"string":"foo"}

これをもうちょっと汎用的にして、

applyObject :: (Object -> Object) -> Value -> Parser Value
applyObject f value = Object . f <$> parseJSON value

main = putStrLn $ LC.unpack $ encode $ forceResult
       $ parse (applyObject (M.delete "object") <=< applyObject (M.delete "array")) value

こんな風に書くこともできます。

まとめ

Data.Aeson 便利です。
しかも JSON データの文字列型には Lazy な ByteString を、配列型には Vector を、オブジェクト型には Map を使っているので、かなり高速なようです。
日本語だと Text.JSON の記事ばっかりで Data.Aeson の記事が全く見つからないのですが、これを機会に Data.Aeson に乗り換えましょう。