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

[Hackageのドキュメント](https://hackage.haskell.org/package/aeson-0.9.0.1/docs/Data-Aeson.html) に結構詳しく使い方が書いてあるので読めばだいたい分かるのですが、日本語でまとまった情報がなさそうだったので、まとめてみました。私もまだHaskellは勉強中なので、何か間違いがあったらご指摘頂ければ幸いです。

なお、対象バージョンは GHC 7.8.3 (Haskell Platform 2014.02)、aeson-0.9.0.1 です。

## JSONデータと定義したデータ型を相互に変換する

大きく分けて、以下の3つの方法があります。

* 自前で変換する関数を定義する
* Generic型クラスを用いた自動導出
* Template Haskellを用いた自動導出

自前で変換する関数を定義するのは基本的な方法で、面倒ですが最も柔軟な方法です。一方、自動導出による方法は、JSONデータと定義したデータ型が単純に変換できる場合に自動的に変換関数を定義してくれます。通常は自動導出を使う方が便利ですが、単純な変換以上のことをしたい場合は自前で変換する関数を定義する必要があります。

### 自前で変換する関数を定義する

```JSON
{"id":123, "content":"Hello"}
```

というJSONデータを

```Haskell
data Foo = Foo { id :: Int, content :: String }
```

というデータ型と相互に変換するコードは、以下のようになります。

```Haskell
{-# 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` 型に変換します。まぁ、機械的に並べるだけなので、何をやっているか分からなくてもコードは書けますが。

この定義したデータ型を使って実際に変換を行うコードは、以下のようになります。

```Haskell
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型クラスを用いた自動導出を行うことで、以下のように変換関数の定義を省略することができます。

```Haskell
{-# 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言語で言うところの「マクロ」で、コンパイル前のプリプロセスでコードを展開するものらしいです。まぁ、やっていることから大体イメージはできると思います。

```Haskell
{-# 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データが

```JSON
{"id":"123", "content":"Hello"}
```

だった場合を考えます。つまり、`id` はJSONデータでは文字列ですが、データ型としては `Int` で扱いたい場合です。この場合は自前で変換関数を定義し、以下のように必要な箇所に変換関数を挿入してやればOKです。

```Haskell
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データのキーと定義したデータ型のフィールド名を一致させておく必要があります。しかし、場合によってはこれを避けたいときがあります。単純な例としては、

```JSON
{"id":123, "data":"Hello"}
```

というJSONデータを変換しようとすると、"data" はHaskellの予約語でフィールド名に使えないので、

```Haskell
data Foo = Foo { id :: Int, data' :: String } deriving Show
```

のように、変換するデータ型にキー名とは異なるフィールド名を使うことになります。このデータ型に対してどのように変換を行うかですが、自前で変換関数定義する場合は特に問題はなく、以下のように定義すればOKです。

```Haskell
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` を上書きすることによって変換できるようにします。例えば

```Haskell
cnvFieldLabel label = if label == "data'" then "data" else label
```

のようにフィールド名を変換する関数を作り、Template Haskellによる自動導出を使う場合は、

```Haskell
$(deriveJSON defaultOptions { fieldLabelModifier = cnvFieldLabel } ''Foo)
```

のようにします。Generic型クラスによる自動導出を使う場合は、双方向の変換関数を定義しないといけないので少し面倒ですが、以下のようになります。

```Haskell
instance FromJSON Foo where
  parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = cnvFieldLabel }

instance ToJSON Foo where
  toJSON = genericToJSON defaultOptions { fieldLabelModifier = cnvFieldLabel }
```

また、上記では特定のフィールドだけ違う名前を使いましたが、よく見るやり方としては以下のように全部のフィールドに特定のprefixを付けてしまう手があります。

```Haskell
data Foo = Foo { fooid :: Int, foodata :: String } deriving Show

$(deriveJSON defaultOptions { fieldLabelModifier = drop 3 } ''Foo)
```

## 場合によって存在しないキーがあるとき

JSONデータの一部のキーが、場合によって存在したりしなかったりすることはよくあります。そのようなキーに対しては、対応するデータ型のフィールドを `Maybe` にしておくことで対処できます。例えば上記の例で `content` が存在しないことがある場合、データ型の定義を

```Haskell
data Foo = Foo { id :: Int, content :: Maybe String }
```

としておきます。その上で、自前で変換関数を定義する場合は以下のようにします。

```Haskell
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` だと変換結果は

```JSON
{"id":123, "content":null}
```

のようになります。`content` キー自体を含まないようにするには、ちょっとうまい方法がありませんが、上記では `maybe` 関数を使って場合分けしています。

自動導出の場合はもっと簡単で、データ型の定義変更以外に何もする必要がありません。ただし、上記のデータ型 => JSON変換時に `Nothing` に対するキーを含まないようにするには、オプション `omitNothingFields` を設定します。

```Haskell
instance ToJSON Foo where
  toJSON = genericToJSON defaultOptions { omitNothingFields = True }
```

```Haskell
$(deriveJSON defaultOptions { omitNothingFields = True } ''Foo)
```