Hackageのドキュメント に結構詳しく使い方が書いてあるので読めばだいたい分かるのですが、日本語でまとまった情報がなさそうだったので、まとめてみました。私もまだHaskellは勉強中なので、何か間違いがあったらご指摘頂ければ幸いです。
なお、対象バージョンは GHC 7.8.3 (Haskell Platform 2014.02)、aeson-0.9.0.1 です。
JSONデータと定義したデータ型を相互に変換する
大きく分けて、以下の3つの方法があります。
- 自前で変換する関数を定義する
- Generic型クラスを用いた自動導出
- Template Haskellを用いた自動導出
自前で変換する関数を定義するのは基本的な方法で、面倒ですが最も柔軟な方法です。一方、自動導出による方法は、JSONデータと定義したデータ型が単純に変換できる場合に自動的に変換関数を定義してくれます。通常は自動導出を使う方が便利ですが、単純な変換以上のことをしたい場合は自前で変換する関数を定義する必要があります。
自前で変換する関数を定義する
{"id":123, "content":"Hello"}
というJSONデータを
data Foo = Foo { id :: Int, content :: String }
というデータ型と相互に変換するコードは、以下のようになります。
{-# LANGUAGE OverloadedStrings #-}
import Data.Aeson
import Control.Applicative
data Foo = Foo { id :: Int, content :: String } deriving Show
instance FromJSON Foo where
parseJSON (Object v) = Foo <$> (v .: "id")
<*> (v .: "content")
instance ToJSON Foo where
toJSON (Foo id content) = object [ "id" .= id,
"content" .= content ]
必要なのは Foo
を FromJSON
と ToJSON
型クラスのインスタンスにすることで、それには parseJSON
関数と toJSON
関数を定義する必要があります。と言っても、単純に変換ができる場合、上記のように機械的に書いていくだけです。ここで「単純に変換ができる」というのは、正確には変換するデータ型の各フィールドの型が FromJSON
及び ToJSON
のインスタンスになっていることで、どの型がインスタンスになっているかは、Hackageのドキュメントに書いてあります。
parseJSON
の定義には、.:
演算子を使います。.:
の戻り値は Parser
モナド型になるので、<$>
と <*>
を使ってApplicatieスタイルで繋いでいくのが常套手段です。一方、toJSON
の方は .=
演算子を使って Pair
型のリストを作り、それを object
関数で Value
型に変換します。まぁ、機械的に並べるだけなので、何をやっているか分からなくてもコードは書けますが。
この定義したデータ型を使って実際に変換を行うコードは、以下のようになります。
main = do
let json = "{\"id\":123, content\":\"Hello\"}"
case decode json :: Maybe Foo of
Nothing -> print "Parse error"
Just x -> print x
let foo = Foo 123 "Hello"
print $ encode foo
JSONデータ => データ型の変換には decode
関数、データ型 => JSONデータの変換には encode
関数を使います。decode
は変換に失敗する可能性があるので、戻り値は Maybe
型になっています。
Generic型クラスを用いた自動導出
上記のように単純に変換が可能な場合、Generic型クラスを用いた自動導出を行うことで、以下のように変換関数の定義を省略することができます。
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveGeneric #-}
import Data.Aeson
import GHC.Generics
data Foo = Foo { id :: Int, content :: String } deriving (Show, Generic)
instance FromJSON Foo
instance ToJSON Foo
Generic型クラスが何をするものなのかは、私もよくわかっていないので説明できませんが、やらないといけないことは
-
DeriveGeneric
言語拡張の宣言追加 -
GHC.Generics
パッケージのインポート - データ型
Foo
のderiving宣言にGeneric
を追加
だけです。
Template Haskellを用いた自動導出
こちらは、Template Haskellを用いて自動導出を行う方法です。Template Haskellというのは、C言語で言うところの「マクロ」で、コンパイル前のプリプロセスでコードを展開するものらしいです。まぁ、やっていることから大体イメージはできると思います。
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
import Data.Aeson
import Data.Aeson.TH
data Foo = Foo { id :: Int, content :: String } deriving Show
$(deriveJSON defaultOptions ''Foo)
やらないといけないことは、
-
TemplateHaskell
言語拡張の宣言追加 -
Data.Aeson.TH
パッケージのインポート -
deriveJSON
関数 (?) の定義
です。できる事自体はGeneric型クラスを用いた自動導出と変わらないと思いますが、後述のカスタマイズまで含めると、こちらの方が記述量が少なくなります。
単純ではない変換が必要なとき
例えば、上記の例でJSONデータが
{"id":"123", "content":"Hello"}
だった場合を考えます。つまり、id
はJSONデータでは文字列ですが、データ型としては Int
で扱いたい場合です。この場合は自前で変換関数を定義し、以下のように必要な箇所に変換関数を挿入してやればOKです。
import Text.Printf
instance FromJSON Foo where
parseJSON (Object v) = Foo <$> (conv <$> (v .: "id"))
<*> (v .: "content")
where conv = read :: String -> Int
instance ToJSON Foo where
toJSON (Foo id content) = object ["id" .= conv id,
"content" .= content ]
where conv = printf "%d" :: Int -> String
ただ、多くの場合、単純でない変換が必要なフィールドは一部だけなので、自動導出した上で必要なフィールドだけ変換できると嬉しいのですが...
JSONのキーとデータ型のフィールドに同じ名前を使いたくないとき
自前で定義するときは問題ありませんが、自動導出を使うときは基本的にJSONデータのキーと定義したデータ型のフィールド名を一致させておく必要があります。しかし、場合によってはこれを避けたいときがあります。単純な例としては、
{"id":123, "data":"Hello"}
というJSONデータを変換しようとすると、"data" はHaskellの予約語でフィールド名に使えないので、
data Foo = Foo { id :: Int, data' :: String } deriving Show
のように、変換するデータ型にキー名とは異なるフィールド名を使うことになります。このデータ型に対してどのように変換を行うかですが、自前で変換関数定義する場合は特に問題はなく、以下のように定義すればOKです。
instance FromJSON Foo where
parseJSON (Object v) = Foo <$> (v .: "id")
<*> (v .: "data")
instance ToJSON Foo where
toJSON (Foo id data') = object [ "id" .= id,
"data" .= data' ]
自動導出の場合は、自動導出に使う関数に与えるオプション内のフィールド fieldLabelModifier
を上書きすることによって変換できるようにします。例えば
cnvFieldLabel label = if label == "data'" then "data" else label
のようにフィールド名を変換する関数を作り、Template Haskellによる自動導出を使う場合は、
$(deriveJSON defaultOptions { fieldLabelModifier = cnvFieldLabel } ''Foo)
のようにします。Generic型クラスによる自動導出を使う場合は、双方向の変換関数を定義しないといけないので少し面倒ですが、以下のようになります。
instance FromJSON Foo where
parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = cnvFieldLabel }
instance ToJSON Foo where
toJSON = genericToJSON defaultOptions { fieldLabelModifier = cnvFieldLabel }
また、上記では特定のフィールドだけ違う名前を使いましたが、よく見るやり方としては以下のように全部のフィールドに特定のprefixを付けてしまう手があります。
data Foo = Foo { fooid :: Int, foodata :: String } deriving Show
$(deriveJSON defaultOptions { fieldLabelModifier = drop 3 } ''Foo)
場合によって存在しないキーがあるとき
JSONデータの一部のキーが、場合によって存在したりしなかったりすることはよくあります。そのようなキーに対しては、対応するデータ型のフィールドを Maybe
にしておくことで対処できます。例えば上記の例で content
が存在しないことがある場合、データ型の定義を
data Foo = Foo { id :: Int, content :: Maybe String }
としておきます。その上で、自前で変換関数を定義する場合は以下のようにします。
instance FromJSON Foo where
parseJSON (Object v) = Foo <$> (v .: "id")
<*> (v .:? "content")
instance ToJSON Foo where
toJSON (Foo id content) =
object $ [ "id" .= id ] ++
maybe [] (\x -> [ "content" .= x ]) content
parseJSON
は、Maybe
型のフィールドのところを .:
から .:?
に変更するだけです。toJSON
の方は何もしなくてもよいのですが、その場合 conetnt
フィールドが Nothing
だと変換結果は
{"id":123, "content":null}
のようになります。content
キー自体を含まないようにするには、ちょっとうまい方法がありませんが、上記では maybe
関数を使って場合分けしています。
自動導出の場合はもっと簡単で、データ型の定義変更以外に何もする必要がありません。ただし、上記のデータ型 => JSON変換時に Nothing
に対するキーを含まないようにするには、オプション omitNothingFields
を設定します。
instance ToJSON Foo where
toJSON = genericToJSON defaultOptions { omitNothingFields = True }
$(deriveJSON defaultOptions { omitNothingFields = True } ''Foo)