在Haskell中使用Aeson解析JSON配置文件

时间:2018-09-29 10:16:22

标签: json haskell

我正在尝试在Haskell中解析JSON,但是找不到有关此任务的任何有用的文档。我无法执行以下小程序:

{-# LANGUAGE OverloadedStrings #-}

import Data.Aeson
import Data.Word (Word16)
import Data.ByteString.Lazy (pack, ByteString)
import Data.ByteString.Char8 ()

data AppConfig = AppConfig {
    database :: DatabaseConfig
} deriving (Show)

data DatabaseConfig = DatabaseConfig {
    host :: String,
    port :: Word,
    username :: String,
    password :: String,
    databaseName :: String
} deriving (Show)

instance FromJSON DatabaseConfig where
    parseJSON (Object obj) = DatabaseConfig
                <$> obj .: "host"
                <*> obj .: "port"
                <*> obj .: "username"
                <*> obj .: "password"
                <*> obj .: "databaseName"
    parseJSON obj = fail $ show obj

instance FromJSON AppConfig where
    parseJSON (Object obj) = AppConfig <$> obj .: "database"
    parseJSON obj = fail $ show obj

config = "{ \"database\": { \"host\": \"db\", \"port\": 1234, \"username\": \"ledger\",  \"password\": \"ledger\", \"database_name\": \"ledger\" } }\""

main :: IO ()
main = do
  let cfg = eitherDecode config
  case cfg of
    Left err -> fail $ "error: " ++ err
    Right ps -> putStrLn $ ps

该错误消息完全没有意义:

  

用户错误(错误:$中的错误:endOfInput)

那么代码有什么问题?

3 个答案:

答案 0 :(得分:2)

如果您查看packData.ByteString的类型签名

pack :: [Word8] -> ByteString

您会看到,它不需要String或实现IsString typeclass的某种类型,而是一个字节列表。让我们通过使用Data.ByteString.Char8代替Data.ByteString来解决此问题。

现在,出现以下错误:

test.hs:36:12: error:
    • Couldn't match type ‘Either String’ with ‘IO’
      Expected type: IO (Either [Char] String)
        Actual type: Either String (Either [Char] String)
    • In a stmt of a 'do' block: cfg <- (eitherDecode config)
      In the expression:
        do cfg <- (eitherDecode config)
           case cfg of
             Left err -> fail $ "error: " ++ err
             Right ps -> putStrLn $ ps
      In an equation for ‘main’:
          main
            = do cfg <- (eitherDecode config)
                 case cfg of
                   Left err -> fail $ "error: " ++ err
                   Right ps -> putStrLn $ ps
   |
36 |   cfg <-  (eitherDecode config)
   |            ^^^^^^^^^^^^^^^^^^^

那是因为对于某些类型eitherDecode configIO a不会导致类型为a的值,而是会导致类型为Either String a的值。因此,我们修复它。

现在出现以下错误:

 test.hs:36:27: error:
    • Couldn't match expected type ‘Data.ByteString.Lazy.Internal.ByteString’
                  with actual type ‘ByteString’
      NB: ‘ByteString’ is defined in ‘Data.ByteString.Internal’
          ‘Data.ByteString.Lazy.Internal.ByteString’
            is defined in ‘Data.ByteString.Lazy.Internal’
    • In the first argument of ‘eitherDecode’, namely ‘config’
      In the expression: (eitherDecode config)
      In an equation for ‘cfg’: cfg = (eitherDecode config)
   |
36 |   let cfg = (eitherDecode config)
   |                           ^^^^^^

很显然,我们选择了错误的ByteString类型。 ByteString中的Data.ByteString.Char8很严格,但是aeson希望使用惰性字节串。因此,让我们使用Data.ByteString.Lazy.Char8来解决此问题。

现在它编译并运行该程序会出现以下错误:

[nix-shell:~/tmp]$ ./test
test: user error (error: Error in $: endOfInput)

这是因为字符串文字中的JSON错误。读

"{ \"database\": { \"host\": \"db\", \"port\": 1234, \"username\": \"ledger\",  \"password\": \"ledger\", \"database_name\": \"ledger\" } }\""

它应显示为:

"{ \"database\": { \"host\": \"db\", \"port\": 1234, \"username\": \"ledger\",  \"password\": \"ledger\", \"database_name\": \"ledger\" } }"

重新编译并运行测试现在会导致以下错误:

[nix-shell:~/tmp]$ ./test
test: user error (error: Error in $: expected String, encountered Object)

因此,显然,aeson认为它应该解码json字符串值,但是遇到了一个对象。如果您看以下几行

let cfg = (eitherDecode config)
case cfg of
   Left err -> fail $ "error: " ++ err
   Right ps -> putStrLn $ ps

您会看到ps被键入为String,因为它被用作putStrLn的自变量,这解释了观察到的行为。如果我们只是将putStrLn $ ps更改为putStrLn $ show ps,则编译器根本不知道ps的类型是什么,所以让我们为他提供类型注释。

重新编译并运行测试现在会导致以下错误:

[nix-shell:~/tmp]$ ./test
test: user error (error: Error in $.database: key "databaseName" not present)

因此您的程序希望databaseName作为json对象中的键,而不是database_name。修复FromJSON实例以解决此问题。

现在它输出:

[nix-shell:~/tmp]$ ./test
AppConfig {database = DatabaseConfig {host = "db", port = 1234, username = "ledger", password = "ledger", databaseName = "ledger"}}

最终程序显示为:

{-# LANGUAGE OverloadedStrings #-}

import Data.Aeson
import Data.Word (Word16)
import Data.ByteString.Lazy.Char8 (pack,ByteString)

data AppConfig = AppConfig {
    database :: DatabaseConfig
} deriving (Show)

data DatabaseConfig = DatabaseConfig {
    host :: String,
    port :: Word,
    username :: String,
    password :: String,
    databaseName :: String
} deriving (Show)

instance FromJSON DatabaseConfig where
    parseJSON (Object obj) = DatabaseConfig
                <$> obj .: "host"
                <*> obj .: "port"
                <*> obj .: "username"
                <*> obj .: "password"
                <*> obj .: "database_name"
    parseJSON obj = fail $ show obj

instance FromJSON AppConfig where
    parseJSON (Object obj) = AppConfig <$> obj .: "database"
    parseJSON obj = fail $ show obj

config = pack "{ \"database\": { \"host\": \"db\", \"port\": 1234, \"username\": \"ledger\",  \"password\": \"ledger\", \"database_name\": \"ledger\" } }"

main :: IO ()
main = do
  let cfg = (eitherDecode config) :: Either String AppConfig
  case cfg of
    Left err -> fail $ "error: " ++ err
    Right ps -> putStrLn $ show ps

答案 1 :(得分:1)

看起来像您输入中的错误,而不是代码中的错误。字符串末尾的\"看起来可疑,该字符串的内容如下:

{ "database": { "host": "db", ... } }"

结尾"引起解析错误的地方。

答案 2 :(得分:0)

hnefati和Krom已经提供了答案(您有一个额外的\“字符,需要在解码函数中添加类型注释,并且需要将database_name与databaseName匹配)。对于此特定示例,如果您满意,使用几种语言扩展,您的代码可以这样简化:

{-# LANGUAGE DeriveAnyClass    #-}
{-# LANGUAGE DeriveGeneric     #-}
{-# LANGUAGE OverloadedStrings #-}

module SO where

import Data.Aeson
import Data.ByteString.Char8 ()
import GHC.Generics          (Generic)

data AppConfig = AppConfig {
    database :: DatabaseConfig
} deriving (Show, Generic, FromJSON)

data DatabaseConfig = DatabaseConfig {
    host         :: String,
    port         :: Word,
    username     :: String,
    password     :: String,
    databaseName :: String
} deriving (Show, Generic, FromJSON)

config = "{ \"database\": { \"host\": \"db\", \"port\": 1234, \"username\": \"ledger\",  \"password\": \"ledger\", \"databaseName\": \"ledger\" } }"

main :: IO ()
main = do
  let cfg = eitherDecode config
  case cfg of
    Left err -> fail $ "error: " ++ err
    Right ps -> putStrLn $ show $ (ps :: AppConfig)