自分のブログ記事からのクロス投稿です。
HaskellのJSONライブラリとしておそらくメジャーなData.Aesonですが、使い方が難しいので色々試行錯誤しています。
以降のコードはghci上で実行することを想定しています。
適当にstackでプロジェクトを作って、aesonをcabalファイルのbuild-dependsに追加してビルドし、stack ghci
とするのが良いと思います。あと、Data.TextやData.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
のインスタンスになっている必要があります。
このように定義します。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"})
このようなデータ型でもこれまでと同じようにToJSON
・FromJSON
のインスタンスにすることができます。自動導出の場合は、以下の例のようにエンコード時に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
型で考えます。自動導出で問題ない場合は良いのですが、自分で定義しようとした時に、少し悩みました。toJSON
もparseJSON
も、a
の部分の値をエンコード・デコードする処理を自分で定義してしまうと型を決めなくてはなりません。そうするとa
のままではできず、Point Int
などの具体的な型で定義せざるを得ません。そこで、a
の部分については、自分で定義せず、toJSON
やparseJSON
をそのまま使います。もともと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
型の場合はどうなるでしょうか。わからないのでまずは自動導出して見ましょう。
:{
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
のような扱いをしたい場合は、専用の関数がすでに用意されているようです。
この場合は、Field String
とField 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
型ですが、前者のtyp
はString
で後者はBool
です。
-- どちらも単にFilter型
:t Filter Title "some title"
-- --> Filter Title "some title" :: Filter
:t Filter Done True
-- --> Filter Done True :: Filter
このようなデータ型の場合、先ほどField
のFromJSON
をField String
とField Bool
でそれぞれ定義した時のようなことができません。field
の値がTitle
ならtyp
はString
、Done
ならBool
というような場合分けをする必要があります。色々試行錯誤してようやくできた定義が以下です。mayTitle
とmayDone
の場合分けのところは、この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
Crieitは誰でも投稿できるサービスです。 是非記事の投稿をお願いします。どんな軽い内容でも投稿できます。
また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!
こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください。
ボードとは?
コメント