LoginSignup
6
4

More than 5 years have passed since last update.

Aeson を使って Haskell で JSON

Last updated at Posted at 2015-05-07

Aeson を調べてみました。

Aeson はいろんな使い方があるけど、Data.Aeson.TH で提供される deriveJSON を使うとはかどると思います。

AesonTest.hs
-- Aeson は ByteString から生成するのでこのプラグマを使用する
{-# LANGUAGE OverloadedStrings #-}
-- Data.Aeson.TH を利用するとはかどるのでこのプラグマを使用する
{-# LANGUAGE TemplateHaskell #-}

import Data.Aeson (decode, encode)
import Data.Aeson.TH (deriveJSON, defaultOptions, Options(..))
import Data.Char (toLower)

data Foo a = Foo { fooId :: a } deriving (Show, Read, Eq)
-- ここ重要!
-- TH を使っているので初見では読めないが、こんな書き方なんだ〜、ぐらいで OK。
-- ポイントは defaultOptions を元に fieldLabelModifier を
-- 先頭 3 文字を削除して小文字に置き換えるようにしていること。
-- こうすると {"fooId":1} という JSON が {"id":1} という JSON になる。
deriveJSON defaultOptions { fieldLabelModifier = map toLower . drop 3 } ''Foo

data Bar = Bar {} deriving (Show, Read, Eq)
deriveJSON defaultOptions ''Bar

data Baz = Baz | Qux  deriving (Show, Read, Eq)
deriveJSON defaultOptions ''Baz

main :: IO ()
main = do
  let json1 = "{\"id\": 1}"
      json2 = "{\"id\": \"2\"}"
      decoded1  = decode json1 :: Maybe (Foo Int) -- 型指定が必要
      decoded1' = decode json1 :: Maybe (Foo String) -- 型指定を誤ると Noghing になる
      decoded2  = decode json2 :: Maybe (Foo Int)
      decoded2' = decode json2 :: Maybe (Foo String)
      encoded1  = encode decoded1
      encoded1' = encode decoded1'
      encoded2  = encode decoded2
      encoded2' = encode decoded2'
      foo3 = Foo 3 :: Foo Int
      foo4 = Foo (Just "4") :: Foo (Maybe String)
      foo5 = Foo (Nothing) :: Foo (Maybe Int)
      encoded3 = encode foo3
      encoded4 = encode foo4
      encoded5 = encode foo5
      json6 = "{\"id\":[]}"
      decoded6   = decode json6 :: Maybe (Foo [Int]) -- 空配列は型指定次第で何にでもなれる
      decoded6'  = decode json6 :: Maybe (Foo String) -- String は配列とはみなされないようだ
      decoded6'' = decode json6 :: Maybe (Foo [String])
      bar7 = Bar
      baz8 = Baz
      qux9 = Qux
      encoded7 = encode bar7 -- "{}" を期待するも "[]"
      encoded8 = encode baz8
      encoded9 = encode qux9
      decoded8  = decode encoded8 :: Maybe Baz -- 何故か Nothing になる
      decoded8' = decode "\"Baz\"" :: Maybe Baz -- こちらでも同様に Nothing になる
      json10 = "{\"id\":{})" -- これを decode する方法がわからない
      decoded10   = decode json10 :: Maybe (Foo ())
      decoded10'  = decode json10 :: Maybe (Foo Bar)
      json11 = "{\"id\":[1,true,\"3\"]}" -- これを decode する方法がわからない
  print decoded1   -- Just (Foo {fooId = 1})
  print decoded1'  -- Nothing
  print decoded2   -- Nothing
  print decoded2'  -- Just (Foo {fooId = "2"})
  print encoded1   -- "{\"id\":1}"
  print encoded1'  -- "null"
  print encoded2   -- "null"
  print encoded2'  -- "{\"id\":\"2\"}"
  print foo3       -- Foo {fooId = 3}
  print foo4       -- Foo {fooId = Just "4"}
  print foo5       -- Foo {fooId = Nothing}
  print encoded3   -- "{\"id\":3}"
  print encoded4   -- "{\"id\":\"4\"}"
  print encoded5   -- "{\"id\":null}"
  print decoded6   -- Just (Foo {fooId = []})
  print decoded6'  -- Nothing
  print decoded6'' -- Just (Foo {fooId = []})
  print bar7       -- Bar
  print encoded7   -- "[]"
  print baz8       -- Baz
  print encoded8   -- "\"Baz\""
  print decoded8   -- Nothing
  print decoded8'  -- Nothing
  print qux9       -- Qux
  print encoded9   -- \"Qux\""
  print decoded10  -- Noghing
  print decoded10' -- Nothing

JSON は Haskell よりも型がゆるいので、すべての JSON が Haskell で扱えるわけではないと思います。特に [1, true, "3"] のような異なる型を持つ配列や、{} のような空のオブジェクトです。

また、JSON の null は Haskell の Nothing にマッピングされるので、null を取りうる値は Maybe 型にしておかなければならず、コードが冗長になってしまいます。

とは言え、どれも設計次第で解決可能なので、Aeson は便利なライブラリです。

追試

型コンストラクタをひとつだけ持つデータと、ふたつ以上持つデータを比較してみました。

結果としては、型コンストラクタをひとつだけ持つデータは [] という JSON にエンコードされ、この JSON をデコードすると、指定した Haskell の型が得られました。

型コンストラクタがふたつ以上あるデータは "Bar" などのコンストラクタの文字列表記の JSON にエンコードされるのですが、この文字列の JSON はデコードすることができず Nothing になります。

どちらの動作も実用的ではないので、不具合ではないかという気がしています。ちなみに利用した Aeson のバージョンは 0.8.0.2 です。

ConstructorDecoder.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}

import Data.Aeson (decode, encode)
import Data.Aeson.TH (deriveJSON, defaultOptions, Options(..))

data Foo = Foo deriving (Show, Read, Eq)
deriveJSON defaultOptions ''Foo

data Bar = Bar | Baz deriving (Show, Read, Eq)
deriveJSON defaultOptions ''Bar

main :: IO ()
main = do
  let encFoo   = encode Foo -- 何故か "[]"
      decFoo   = decode encFoo :: Maybe Foo -- Just Foo
      decFoo'  = decode "[]" :: Maybe Foo -- Just Foo
      decFoo'' = decode "\"Foo\"" :: Maybe Foo -- Nothing
  print encFoo
  print decFoo
  print decFoo'
  print decFoo''
  let encBar   = encode Bar -- "\"Bar\""
      decBar   = decode encBar :: Maybe Bar -- Nothing
      decBar'  = decode "\"Bar\"" :: Maybe Bar -- Nothing
      decBar'' = decode "[]" :: Maybe Bar -- Nothing
  print encBar
  print decBar
  print decBar'
  print decBar''
6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4