AesonでJSONをパース・生成する方法まとめ

  • 21
    Like
  • 0
    Comment
More than 1 year has passed since last update.

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 ]

必要なのは FooFromJSONToJSON 型クラスのインスタンスにすることで、それには 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)