1. alpha22jp

    Posted

    alpha22jp
Changes in title
+AesonでJSONをパース・生成する方法まとめ
Changes in tags
Changes in body
Source | HTML | Preview
@@ -0,0 +1,222 @@
+[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` を上書きすることによって変換できるようにします。Template Haskellによる自動導出を使う場合は、以下のようになります。
+
+```Haskell
+$(deriveJSON defaultOptions {
+ fieldLabelModifier = \label -> if label == "data'" then "data" else label } ''Foo)
+```
+
+Generic型クラスによる自動導出を使う場合は、逆方向の変換関数も定義しないといけないので少し面倒ですが、以下のようになります。
+
+```Haskell
+cnvFieldLabel label = if label == "data'" then "data" else label
+
+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)
+```