2019-09-29に投稿

Data.Aesonの使い方

読了目安:21分

自分のブログ記事からのクロス投稿です。

HaskellのJSONライブラリとしておそらくメジャーなData.Aesonですが、使い方が難しいので色々試行錯誤しています。

以降のコードはghci上で実行することを想定しています。
適当にstackでプロジェクトを作って、aesonをcabalファイルのbuild-dependsに追加してビルドし、stack ghciとするのが良いと思います。あと、Data.TextData.HashMapなども必要です。

# ここはターミナルで
stack new sandbox-prj simple
cd sandbox-prj
# cabalファイルのbuild-dependsにaeson, text, unordered-containers, vectorを追記
stack build
stack ghci

はじめに、以下をghci上でコピペしておいてください。以降のコードが全部動くはずです。

-- ghci上で
-- 最初にこれらをコピペしておくと、以降のコードが全部動くはず

-- 後でGHC拡張を使う
:set -XDeriveGeneric
:set -XExistentialQuantification
:set -XFlexibleContexts
:set -XFlexibleInstances
:set -XGADTs
:set -XInstanceSigs
:set -XOverloadedStrings
:set -XTypeFamilies
:set +m

-- 後で色々使う
import Control.Applicative (empty, pure)
import Data.Aeson
import qualified Data.Aeson.Encoding.Internal as DAE
import Data.Aeson.Types
import Data.Char (toLower, toUpper)
import qualified Data.HashMap.Lazy as HML
import qualified Data.HashMap.Strict as HMS
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import qualified Data.Vector as V
import GHC.Generics

基本的な使い方

基本的な型については特にパーサーを定義しなくて良いので、いきなりエンコード・デコードできます。

エンコード

encode 1
-- --> "1"
encode "hello"
-- --> "\"hello\""
encode [1,2,3]
-- --> "[1,2,3]"
encode (1,2)
-- --> "[1,2]"
encode (Just 1)
-- --> "1"
encode True
-- --> "true"
encode Null
-- --> "null"
encode (1234, "hello", True, False, [1,2,3], Just 10)
-- --> "[1234,\"hello\",true,false,[1,2,3],10]"

デコードは、Haskellの型を指定する必要があります。

-- 型を指定しないとデコードできない
decode "1"
-- --> Nothing

decode "1" :: Maybe Int
-- --> Just 1
decode "\"hello\"" :: Maybe String
-- --> Just "hello"
decode "[1234,\"hello\",true,false,[1,2,3],10]" :: Maybe (Int,String,Bool,Bool,[Int],Maybe Int)
-- --> Just (1234,"hello",True,False,[1,2,3],Just 10)

ToJSON, FromJSON

エンコード・デコードするためには、そのデータ型がToJSONFromJSONのインスタンスになっている必要があります。
このように定義します。DeriveGeneric拡張(はじめにセットしたやつ)とGHC.Genericsが必要です。

data MyData = MyData { field :: String } deriving (Show, Generic)
instance FromJSON MyData
instance ToJSON MyData

myData = MyData "my data!!"
encode myData
-- --> "{\"field\":\"my data!!\"}"
decode "{\"field\":\"my data!!\"}" :: Maybe MyData
-- --> Just (MyData {field = "my data!!"})

簡単ですね。基本的には、エンコード・デコードの実装を自分で定義しなくても自動的に導出してくれます。

-- :{ :} はghciで複数行定義をするときに使う記号
:{
data Person = Person
  { name :: String
  , age :: Int
  , group :: [String]
  } deriving (Show, Generic)
:}
instance FromJSON Person
instance ToJSON Person

p1 = Person "Neo" 30 ["groupA", "groupB"]
p2 = Person "Morpheus" 40 ["groupA"]
p3 = Person "Trinity" 30 []

encode p1
-- --> "{\"group\":[\"groupA\",\"groupB\"],\"age\":30,\"name\":\"Neo\"}"

decode "{\"group\":[\"groupA\",\"groupB\"],\"age\":30,\"name\":\"Neo\"}" :: Maybe Person
-- --> Just (Person {name = "Neo", age = 30, group = ["groupA","groupB"]})

-- リストはもともとToJSONとFromJSONのインスタンスなので、
encode [p1, p2, p3]
-- --> "[{\"group\":[\"groupA\",\"groupB\"],\"age\":30,\"name\":\"Neo\"},{\"group\":[\"groupA\"],\"age\":40,\"name\":\"Morpheus\"},{\"group\":[],\"age\":30,\"name\":\"Trinity\"}]"

decode "[{\"group\":[\"groupA\",\"groupB\"],\"age\":30,\"name\":\"Neo\"},{\"group\":[\"groupA\"],\"age\":40,\"name\":\"Morpheus\"},{\"group\":[],\"age\":30,\"name\":\"Trinity\"}]" :: Maybe [Person]
-- --> Just [Person {name = "Neo", age = 30, group = ["groupA","groupB"]},Person {name = "Morpheus", age = 40, group = ["groupA"]},Person {name = "Trinity", age = 30, group = []}]

※ 彼らの年齢は間違っているかもしれません。

Haskellのレコードを使うときは、以下のように型名のプレフィックスをフィールド名につけて定義することが多いかもしれません(あるいはlensなどを使うのかもしれませんが)。それでも、JSONのフィールド名はシンプルに"title", "created"としておきたいですよね。
こういう場合、defaultOptionsというのを使います。fieldLabelModifierに、フィールド名の変換関数を定義しておけば、エンコード時に適用してくれます。ここでは、プレフィックス("movie"の5文字)を取り除き、かつ小文字にするというルールにしました。

:{
data Movie = Movie
  { movieTitle :: String
  , movieCreated :: Int
  } deriving (Show, Generic)
:}
movieOptions = defaultOptions {fieldLabelModifier = map toLower . drop 5}
instance ToJSON Movie where
  toEncoding = genericToEncoding movieOptions
instance FromJSON Movie where
  parseJSON = genericParseJSON movieOptions


movie = Movie "The Matrix" 1999

encode movie
-- --> "{\"title\":\"The Matrix\",\"created\":1999}"

decode "{\"title\":\"The Matrix\",\"created\":1999}" :: Maybe Movie
-- --> Just (Movie {movieTitle = "The Matrix", movieCreated = 1999})

エンコーダー・デコーダーを自前で定義

では、代数データ型の場合はどうでしょうか。基本的には以下のように自動導出できます。

data Size = Small|Medium|Large deriving (Show, Generic)
instance ToJSON Size
instance FromJSON Size

encode [Small, Medium, Large]
-- --> "[\"Small\",\"Medium\",\"Large\"]"

decode "[\"Small\",\"Medium\",\"Large\"]" :: Maybe [Size]
-- --> Just [Small,Medium,Large]

ただ、JSONデータではキャメルケースでなくsmall, medium, largeのように小文字で表現したいかもしれません。その場合は、以下のように自分で定義する必要があります。ToJSONのインスタンスにするには、toJSONか、toEncodingを自分で定義します。以下のような型です。

toJSON :: a -> Value
toEncoding :: a -> Encoding

toJSONは任意の型をData.AesonのValue型にします。したがって、toJSONのみを定義した場合は、toEncodingが自動導出されます。JSONにエンコードするときは、一旦Valueに変換され、その後JSONの文字列に変換されます。
一方、toEncodingを直接定義することもできて、その場合は、Valueを経由せず直接エンコードされることになります。ここには歴史的経緯があるようです。

-- toJSONを定義する場合
data Size = Small|Medium|Large deriving (Show, Generic)
instance ToJSON Size where
  toJSON Small = String "small"
  toJSON Medium = String "medium"
  toJSON Large = String "large"

encode [Small, Medium, Large]
-- --> "[\"small\",\"medium\",\"large\"]"
-- toEncodingを定義する場合
data Size = Small|Medium|Large deriving (Show, Generic)
instance ToJSON Size where
  toEncoding Small = DAE.string "small"
  toEncoding Medium = DAE.string "medium"
  toEncoding Large = DAE.string "large"

encode [Small, Medium, Large]
-- --> "[\"small\",\"medium\",\"large\"]"

FromJSONのインスタンスについては、parseJSONを定義します。こちらは少し面倒です。以下のような型をしています。

parseJSON :: Value -> Parser a
-- withTextの第一引数("Size")は、どんな文字列でも良いみたい。
-- なんのための引数なのかまだわかっていない。
instance FromJSON Size where
  parseJSON = withText "Size" $ \t ->
    fromString (TL.unpack (TL.fromStrict t))
    where fromString :: String -> Parser Size
          fromString "small" = pure Small
          fromString "medium" = pure Medium
          fromString "large" = pure Large
          fromString _ = empty

decode "[\"small\",\"medium\",\"large\"]" :: Maybe [Size]
-- --> Just [Small,Medium,Large]

これで、Sizeデータを使ったレコードもエンコード・デコードすることができます。

:{
data Tomato = Tomato
  { size :: Size
  , brand :: String
  } deriving (Show, Generic)
:}
instance ToJSON Tomato
instance FromJSON Tomato

mini = Tomato Small "famous brand name"

encode mini
-- --> "{\"size\":\"small\",\"brand\":\"famous brand name\"}"

decode "{\"size\":\"small\",\"brand\":\"famous brand name\"}"
-- --> Just (Tomato {size = Small, brand = "famous brand name"})

多相型

このようなデータ型でもこれまでと同じようにToJSONFromJSONのインスタンスにすることができます。自動導出の場合は、以下の例のようにエンコード時にWrapが外れます。

data Wrap a = Wrap a deriving (Show, Generic)
instance ToJSON a => ToJSON (Wrap a)
instance FromJSON a => FromJSON (Wrap a)

encode $ Wrap 1
-- --> "1"
encode $ Wrap "wrapped"
-- --> "\"wrapped\""

decode "1" :: Maybe (Wrap Int)
-- --> Just (Wrap 1)
decode "\"wrapped\"" :: Maybe (Wrap String)
-- --> Just (Wrap "wrapped")

今度はPoint a型で考えます。自動導出で問題ない場合は良いのですが、自分で定義しようとした時に、少し悩みました。toJSONparseJSONも、aの部分の値をエンコード・デコードする処理を自分で定義してしまうと型を決めなくてはなりません。そうするとaのままではできず、Point Intなどの具体的な型で定義せざるを得ません。そこで、aの部分については、自分で定義せず、toJSONparseJSONをそのまま使います。もともとaはそれらのインスタンスであることを仮定しているので、自分で定義する必要がありません。以下のように書くと、型引数をとる型についても定義できます。

data Point a = Point a a deriving (Show, Generic)
instance ToJSON a => ToJSON (Point a) where
  toJSON (Point x y) = Object $ HMS.fromList [("point", toJSON [x,y])]

:{
instance FromJSON a => FromJSON (Point a) where
  parseJSON (Object o) = case HML.lookup "point" o of
    Just arr -> let
                    pts = parseJSON arr :: FromJSON b => Parser (b, b)
                in
                    Point <$> (fst <$> pts) <*> (snd <$> pts)
    Nothing -> empty
:}

pi = Point 1 2
ps = Point "one" "two"

encode pi
-- --> "{\"point\":[1,2]}"

encode ps
-- --> "{\"point\":[\"one\",\"two\"]}"

decode "{\"point\":[1,2]}" :: Maybe (Point Int)
-- --> Just (Point 1 2)

decode "{\"point\":[\"one\",\"two\"]}" :: Maybe (Point String)
-- --> Just (Point "one" "two")

少し複雑な代数データ型

data Gender = Female|Male deriving (Show, Generic)
data Property = Name String|Age Int|Gender Gender deriving (Show, Generic)
instance ToJSON Gender
instance FromJSON Gender
instance ToJSON Property
instance FromJSON Property

props = [Name "Neo", Age 30, Gender Male]

encode props
-- --> "[{\"tag\":\"Name\",\"contents\":\"Neo\"},{\"tag\":\"Age\",\"contents\":30},{\"tag\":\"Gender\",\"contents\":\"Male\"}]"

decode "[{\"tag\":\"Name\",\"contents\":\"Neo\"},{\"tag\":\"Age\",\"contents\":30},{\"tag\":\"Gender\",\"contents\":\"Male\"}]" :: Maybe [Property]
-- --> Just [Name "Neo",Age 30,Gender Male]

このような型でもすべて自動導出できました。しかし、JSONのフォーマットは微妙ですね。以下のようになって欲しいです。(レコード型で定義すれば良いだけの話なのですが、今は例ということで。。)

{ "name": "Neo", "age": 30, "gender": "male" }

自前で定義しましょう。名前を変えるため別の例にしますが、データ構造はほぼ一緒です。

data CarType = Coupe|SUV deriving (Show, Generic)
data Car = Model String|Release Int|CarType CarType deriving (Show, Generic)

instance ToJSON CarType where
  toJSON Coupe = String "coupe"
  toJSON SUV = String "suv"

instance FromJSON CarType where
  parseJSON = withText "CarType" $ \t ->
    fromString (T.unpack t)
    where fromString :: String -> Parser CarType
          fromString "coupe" = pure Coupe
          fromString "suv" = pure SUV
          fromString _ = empty

instance ToJSON Car where
  toJSON (Model s) = object [("model", toJSON s)]
  toJSON (Release i) = object [("release", toJSON i)]
  toJSON (CarType c) = object [("car_type", toJSON c)]

instance FromJSON Car where
  parseJSON (Object o) = case HML.lookup "model" o of
    Just (String s) -> pure (Model $ T.unpack s)
    _ -> case HML.lookup "release" o of
      Just i -> Release <$> (parseJSON i :: Parser Int)
      _ -> case HML.lookup "car_type" o of
        Just t -> CarType <$> (parseJSON t :: Parser CarType)

myCar = [Model "Land Cruiser", Release 1954, CarType SUV]

encode myCar
-- --> "[{\"model\":\"Land Cruiser\"},{\"release\":1954},{\"car_type\":\"suv\"}]"

decode "[{\"model\":\"Land Cruiser\"},{\"release\":1954},{\"car_type\":\"suv\"}]" :: Maybe [Car]
-- --> Just [Model "Land Cruiser",Release 1954,CarType SUV]

これで、望みのJSON形式でエンコード・デコードできるようになりました。

Either型

Either型の場合はどうなるでしょうか。わからないのでまずは自動導出して見ましょう。

:{
data Filter = Filter
  { value :: Either String [String] } deriving (Show, Generic)
:}
instance ToJSON Filter
instance FromJSON Filter

filterL = Filter (Left "eq")
filterR = Filter (Right ["eq", "gt"])

encode filterL
-- --> "{\"value\":{\"Left\":\"eq\"}}"

encode filterR
-- --> "{\"value\":{\"Right\":[\"eq\",\"gt\"]}}"

decode "{\"value\":{\"Left\":\"eq\"}}" :: Maybe (Filter)
-- --> Just (Filter {value = Left "eq"})

decode "{\"value\":{\"Right\":[\"eq\",\"gt\"]}}" :: Maybe (Filter)
-- --> Just (Filter {value = Right ["eq","gt"]})

一応できました。しかし、JSONデータの中に"Left"や"Right"が入ってしまって冗長です。この場合は、以下のようになって欲しいですね。

# Leftの場合
{"value": "eq"}

# Rightの場合
{"value": ["eq", "gt"]}

以下のように定義しましょう。別の例にするのが面倒なので、同じデータを再定義します。

:{
data Filter = Filter
  { value :: Either String [String] } deriving (Show, Generic)
:}
instance ToJSON Filter where
  toJSON (Filter (Left s)) = object [("value", toJSON s)]
  toJSON (Filter (Right ss)) = object [("value", toJSON ss)]

-- Arrayの時に、判定できていないのが難点
-- でも、失敗した時はちゃんとNothingを返してくれるのでとりあえず良いか
instance FromJSON Filter where
  parseJSON (Object o) = case HML.lookup "value" o of
    Just (String s) -> pure (Filter (Left $ T.unpack s))
    Just arr -> (Filter . Right) <$> (parseJSON arr :: Parser [String])
    _ -> empty

filterL = Filter (Left "eq")
filterR = Filter (Right ["eq", "gt"])

encode filterL
-- --> "{\"value\":\"eq\"}"

encode filterR
-- --> "{\"value\":[\"eq\",\"gt\"]}"

decode "{\"value\":\"eq\"}" :: Maybe Filter
-- --> Just (Filter {value = Left "eq"})

decode "{\"value\":[\"eq\",\"gt\"]}" :: Maybe Filter
-- --> Just (Filter {value = Right ["eq","gt"]})

Either型の場合もこのように定義することで自由にJSONとの変換ができます。ちなみに上記の例のようにではなく、Either String aとしてエラーメッセージ付きのMaybeのような扱いをしたい場合は、専用の関数がすでに用意されているようです。

GADT

この場合は、Field StringField Boolの場合でそれぞれ定義するしかなさそうです。もしまとめて定義する方法があれば誰か教えてください。

data Field typ = (typ ~ String) => Title | (typ ~ Bool) => Done

instance Show (Field typ) where
  show Title = "Title"
  show Done = "Done"

instance ToJSON (Field typ) where
  toJSON Title = String "title"
  toJSON Done = String "done"

:{
instance FromJSON (Field String) where
  parseJSON = withText "Field" $ \v -> case v of
    "title" -> pure Title
    _ -> empty
instance FromJSON (Field Bool) where
  parseJSON = withText "Field" $ \v -> case v of
    "done" -> pure Done
    _ -> empty
:}
encode Title
-- --> "\"title\""
encode Done
-- --> "\"done\""
decode "\"title\"" :: Maybe (Field String)
-- --> Just Title
decode "\"done\"" :: Maybe (Field Bool)
-- --> Just Done

で、このFieldを使って、以下のようなレコードを定義します。

:{
data Filter = forall typ. Show typ => Filter
            { field :: Field typ
            , value :: typ
            }
:}

なぜこんなデータ型を考えているかというと、実はpersistentのクラスに、Filterっていうのがあって、これをFromJSONのインスタンスにしたかったのです。このデータをもう少し簡易にしたものが、上で定義したFilterです。ここでの肝は、型引数のtypが型としては明示的に現れていない点です。つまりFilter Title "some title"も、Filter Done Trueも型としては単にFilter型ですが、前者のtypStringで後者はBoolです。

-- どちらも単にFilter型
:t Filter Title "some title"
-- --> Filter Title "some title" :: Filter

:t Filter Done True
-- --> Filter Done True :: Filter

このようなデータ型の場合、先ほどFieldFromJSONField StringField Boolでそれぞれ定義した時のようなことができません。fieldの値がTitleならtypStringDoneならBoolというような場合分けをする必要があります。色々試行錯誤してようやくできた定義が以下です。mayTitlemayDoneの場合分けのところは、この2つデータの型が違うので、それぞれ別の変数にせざるを得ませんでした。そのためmayFieldを必ず2回評価することになり、無駄の多い実装になってしまいました。

instance FromJSON Filter where
        parseJSON (Object o) = do
            let mayField = HMS.lookup "field" o
            let mayValue = HMS.lookup "value" o
            let mayTitle = case mayField of
                               Just (String "title") -> Just Title
                               _ -> Nothing
            let mayDone = case mayField of
                              Just (String "done") -> Just Done
                              _ -> Nothing
            case mayValue of
                Just value -> do
                    case mayTitle of
                        Just title -> Filter title <$> (parseJSON value :: Parser String)
                        Nothing -> case mayDone of
                                       Just done -> Filter done <$> (parseJSON value :: Parser Bool)
                                       Nothing -> empty
                Nothing -> empty

-- Filter Title "some title"
case decode "{\"field\":\"title\", \"value\":\"some title\"}" :: Maybe Filter of
    Just (Filter field value) -> print value
    Nothing -> print Nothing

-- Filter Done True
case decode "{\"field\":\"done\", \"value\":true}" :: Maybe Filter of
    Just (Filter field value) -> print value
    Nothing -> print Nothing
Originally published at www.leo-leo.uno

reouno

Crieitは個人で開発中です。 興味がある方は是非記事の投稿をお願いします! どんな軽い内容でも嬉しいです。
なぜCrieitを作ろうと思ったか

また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!

こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください!

ボードとは?

関連記事

コメント