tag:crieit.net,2005:https://crieit.net/tags/Aeson/feed 「Aeson」の記事 - Crieit Crieitでタグ「Aeson」に投稿された最近の記事 2019-09-29T19:46:32+09:00 https://crieit.net/tags/Aeson/feed tag:crieit.net,2005:PublicArticle/15437 2019-09-29T19:46:32+09:00 2019-09-29T19:46:32+09:00 https://crieit.net/posts/Data-Aeson Data.Aesonの使い方 <p><a target="_blank" rel="nofollow noopener" href="https://www.leo-leo.uno/2019/02/09/798/">自分のブログ記事</a>からのクロス投稿です。</p> <p>HaskellのJSONライブラリとしておそらくメジャーな<a target="_blank" rel="nofollow noopener" href="http://hackage.haskell.org/package/aeson">Data.Aeson</a>ですが、使い方が難しいので色々試行錯誤しています。</p> <p>以降のコードはghci上で実行することを想定しています。<br /> 適当にstackでプロジェクトを作って、aesonをcabalファイルのbuild-dependsに追加してビルドし、<code>stack ghci</code>とするのが良いと思います。あと、<a target="_blank" rel="nofollow noopener" href="http://hackage.haskell.org/package/text">Data.Text</a>や<a target="_blank" rel="nofollow noopener" href="http://hackage.haskell.org/package/unordered-containers">Data.HashMap</a>なども必要です。</p> <pre><code class="shell"># ここはターミナルで stack new sandbox-prj simple cd sandbox-prj # cabalファイルのbuild-dependsにaeson, text, unordered-containers, vectorを追記 stack build stack ghci </code></pre> <p>はじめに、以下をghci上でコピペしておいてください。以降のコードが全部動くはずです。</p> <pre><code class="haskell">-- 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 </code></pre> <h3 id="基本的な使い方"><a href="#%E5%9F%BA%E6%9C%AC%E7%9A%84%E3%81%AA%E4%BD%BF%E3%81%84%E6%96%B9">基本的な使い方</a></h3> <p>基本的な型については特にパーサーを定義しなくて良いので、いきなりエンコード・デコードできます。</p> <p>エンコード</p> <pre><code class="haskell">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]" </code></pre> <p>デコードは、Haskellの型を指定する必要があります。</p> <pre><code class="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) </code></pre> <h3 id="ToJSON, FromJSON"><a href="#ToJSON%2C+FromJSON">ToJSON, FromJSON</a></h3> <p>エンコード・デコードするためには、そのデータ型が<code>ToJSON</code>・<code>FromJSON</code>のインスタンスになっている必要があります。<br /> このように定義します。<code>DeriveGeneric</code>拡張(はじめにセットしたやつ)と<code>GHC.Generics</code>が必要です。</p> <pre><code class="haskell">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!!"}) </code></pre> <p>簡単ですね。基本的には、エンコード・デコードの実装を自分で定義しなくても自動的に導出してくれます。</p> <pre><code class="haskell">-- :{ :} は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 = []}] </code></pre> <p>※ 彼らの年齢は間違っているかもしれません。</p> <p>Haskellのレコードを使うときは、以下のように型名のプレフィックスをフィールド名につけて定義することが多いかもしれません(あるいはlensなどを使うのかもしれませんが)。それでも、JSONのフィールド名はシンプルに"title", "created"としておきたいですよね。<br /> こういう場合、<a target="_blank" rel="nofollow noopener" href="http://hackage.haskell.org/package/aeson-1.4.2.0/docs/Data-Aeson.html#v:defaultOptions"><code>defaultOptions</code></a>というのを使います。<code>fieldLabelModifier</code>に、フィールド名の変換関数を定義しておけば、エンコード時に適用してくれます。ここでは、プレフィックス("movie"の5文字)を取り除き、かつ小文字にするというルールにしました。</p> <pre><code class="haskell">:{ 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}) </code></pre> <h3 id="エンコーダー・デコーダーを自前で定義"><a href="#%E3%82%A8%E3%83%B3%E3%82%B3%E3%83%BC%E3%83%80%E3%83%BC%E3%83%BB%E3%83%87%E3%82%B3%E3%83%BC%E3%83%80%E3%83%BC%E3%82%92%E8%87%AA%E5%89%8D%E3%81%A7%E5%AE%9A%E7%BE%A9">エンコーダー・デコーダーを自前で定義</a></h3> <p>では、代数データ型の場合はどうでしょうか。基本的には以下のように自動導出できます。</p> <pre><code class="haskell">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] </code></pre> <p>ただ、JSONデータではキャメルケースでなく<code>small</code>, <code>medium</code>, <code>large</code>のように小文字で表現したいかもしれません。その場合は、以下のように自分で定義する必要があります。<code>ToJSON</code>のインスタンスにするには、<code>toJSON</code>か、<code>toEncoding</code>を自分で定義します。以下のような型です。</p> <pre><code class="haskell">toJSON :: a -> Value toEncoding :: a -> Encoding </code></pre> <p><code>toJSON</code>は任意の型をData.Aesonの<code>Value</code>型にします。したがって、<code>toJSON</code>のみを定義した場合は、<code>toEncoding</code>が自動導出されます。JSONにエンコードするときは、一旦<code>Value</code>に変換され、その後JSONの文字列に変換されます。<br /> 一方、<code>toEncoding</code>を直接定義することもできて、その場合は、<code>Value</code>を経由せず直接エンコードされることになります。ここには<a target="_blank" rel="nofollow noopener" href="http://hackage.haskell.org/package/aeson-1.4.2.0/docs/Data-Aeson.html#g:7">歴史的経緯</a>があるようです。</p> <pre><code class="haskell">-- 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\"]" </code></pre> <pre><code class="haskell">-- 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\"]" </code></pre> <p><code>FromJSON</code>のインスタンスについては、<code>parseJSON</code>を定義します。こちらは少し面倒です。以下のような型をしています。</p> <pre><code class="haskell">parseJSON :: Value -> Parser a </code></pre> <pre><code class="haskell">-- 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] </code></pre> <p>これで、<code>Size</code>データを使ったレコードもエンコード・デコードすることができます。</p> <pre><code class="haskell">:{ 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"}) </code></pre> <h3 id="多相型"><a href="#%E5%A4%9A%E7%9B%B8%E5%9E%8B">多相型</a></h3> <p>このようなデータ型でもこれまでと同じように<code>ToJSON</code>・<code>FromJSON</code>のインスタンスにすることができます。自動導出の場合は、以下の例のようにエンコード時に<code>Wrap</code>が外れます。</p> <pre><code class="haskell">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") </code></pre> <p>今度は<code>Point a</code>型で考えます。自動導出で問題ない場合は良いのですが、自分で定義しようとした時に、少し悩みました。<code>toJSON</code>も<code>parseJSON</code>も、<code>a</code>の部分の値をエンコード・デコードする処理を自分で定義してしまうと型を決めなくてはなりません。そうすると<code>a</code>のままではできず、<code>Point Int</code>などの具体的な型で定義せざるを得ません。そこで、<code>a</code>の部分については、自分で定義せず、<code>toJSON</code>や<code>parseJSON</code>をそのまま使います。もともと<code>a</code>はそれらのインスタンスであることを仮定しているので、自分で定義する必要がありません。以下のように書くと、型引数をとる型についても定義できます。</p> <pre><code>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") </code></pre> <h3 id="少し複雑な代数データ型"><a href="#%E5%B0%91%E3%81%97%E8%A4%87%E9%9B%91%E3%81%AA%E4%BB%A3%E6%95%B0%E3%83%87%E3%83%BC%E3%82%BF%E5%9E%8B">少し複雑な代数データ型</a></h3> <pre><code class="haskell">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] </code></pre> <p>このような型でもすべて自動導出できました。しかし、JSONのフォーマットは微妙ですね。以下のようになって欲しいです。(レコード型で定義すれば良いだけの話なのですが、今は例ということで。。)</p> <pre><code class="json">{ "name": "Neo", "age": 30, "gender": "male" } </code></pre> <p>自前で定義しましょう。名前を変えるため別の例にしますが、データ構造はほぼ一緒です。</p> <pre><code class="haskell">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] </code></pre> <p>これで、望みのJSON形式でエンコード・デコードできるようになりました。</p> <h3 id="Either型"><a href="#Either%E5%9E%8B">Either型</a></h3> <p><code>Either</code>型の場合はどうなるでしょうか。わからないのでまずは自動導出して見ましょう。</p> <pre><code class="haskell">:{ 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\"<span>}</span><span>}</span>" encode filterR -- --> "{\"value\":{\"Right\":[\"eq\",\"gt\"]<span>}</span><span>}</span>" decode "{\"value\":{\"Left\":\"eq\"<span>}</span><span>}</span>" :: Maybe (Filter) -- --> Just (Filter {value = Left "eq"}) decode "{\"value\":{\"Right\":[\"eq\",\"gt\"]<span>}</span><span>}</span>" :: Maybe (Filter) -- --> Just (Filter {value = Right ["eq","gt"]}) </code></pre> <p>一応できました。しかし、JSONデータの中に"Left"や"Right"が入ってしまって冗長です。この場合は、以下のようになって欲しいですね。</p> <pre><code class="json"># Leftの場合 {"value": "eq"} # Rightの場合 {"value": ["eq", "gt"]} </code></pre> <p>以下のように定義しましょう。別の例にするのが面倒なので、同じデータを再定義します。</p> <pre><code>:{ 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"]}) </code></pre> <p><code>Either</code>型の場合もこのように定義することで自由にJSONとの変換ができます。ちなみに上記の例のようにではなく、<code>Either String a</code>としてエラーメッセージ付きの<code>Maybe</code>のような扱いをしたい場合は、<a target="_blank" rel="nofollow noopener" href="https://www.stackage.org/haddock/lts-13.6/aeson-1.4.2.0/Data-Aeson-Types.html#v:parseEither">専用の関数がすでに用意されている</a>ようです。</p> <h3 id="GADT"><a href="#GADT">GADT</a></h3> <p>この場合は、<code>Field String</code>と<code>Field Bool</code>の場合でそれぞれ定義するしかなさそうです。もしまとめて定義する方法があれば誰か教えてください。</p> <pre><code>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 </code></pre> <p>で、この<code>Field</code>を使って、以下のようなレコードを定義します。</p> <pre><code>:{ data Filter = forall typ. Show typ => Filter { field :: Field typ , value :: typ } :} </code></pre> <p>なぜこんなデータ型を考えているかというと、実は<a target="_blank" rel="nofollow noopener" href="http://hackage.haskell.org/package/persistent">persistent</a>のクラスに、<a target="_blank" rel="nofollow noopener" href="https://github.com/yesodweb/persistent/blob/master/persistent/Database/Persist/Class/PersistEntity.hs#L135">Filter</a>っていうのがあって、これを<code>FromJSON</code>のインスタンスにしたかったのです。このデータをもう少し簡易にしたものが、上で定義した<code>Filter</code>です。ここでの肝は、型引数の<code>typ</code>が型としては明示的に現れていない点です。つまり<code>Filter Title "some title"</code>も、<code>Filter Done True</code>も型としては単に<code>Filter</code>型ですが、前者の<code>typ</code>は<code>String</code>で後者は<code>Bool</code>です。</p> <pre><code>-- どちらも単にFilter型 :t Filter Title "some title" -- --> Filter Title "some title" :: Filter :t Filter Done True -- --> Filter Done True :: Filter </code></pre> <p>このようなデータ型の場合、先ほど<code>Field</code>の<code>FromJSON</code>を<code>Field String</code>と<code>Field Bool</code>でそれぞれ定義した時のようなことができません。<code>field</code>の値が<code>Title</code>なら<code>typ</code>は<code>String</code>、<code>Done</code>なら<code>Bool</code>というような場合分けをする必要があります。色々試行錯誤してようやくできた定義が以下です。<code>mayTitle</code>と<code>mayDone</code>の場合分けのところは、この2つデータの型が違うので、それぞれ別の変数にせざるを得ませんでした。そのため<code>mayField</code>を必ず2回評価することになり、無駄の多い実装になってしまいました。</p> <pre><code>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 </code></pre> reouno