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
こんな風に書くこともできます。