Search code examples
haskellaeson

Trying to get an object out of json using aeson


I am trying to get a Config data record out of the following code:

data Connections = Connections { cfgProperty  :: !Object
                   , connectionName :: String
                   } deriving (Show, Generic)

data Config = Config {connections :: [Connections]} deriving (Show, Generic)

data Cfg = Cfg { config :: Config } deriving (Show, Generic)



instance FromJSON Cfg
instance FromJSON Config
instance FromJSON Connections
instance ToJSON Cfg
instance ToJSON Config
instance ToJSON Connections

jsonFile :: FilePath
jsonFile = "config/config.json"

getCfg :: IO B.ByteString
getCfg = B.readFile jsonFile


parseCfg = do
      j <- (A.eitherDecode <$> getCfg) :: IO (Either String Cfg)
      case j of
        Left err ->  liftIO $ putStrLn err
        Right j -> config j

I'm getting the following error:

/apps/workspace/hade/src/Actor/MasterActor.hs: 56, 20
• Couldn't match expected type ‘IO ()’ with actual type ‘Config’
• In the expression: config j
  In a case alternative: Right j -> config j
  In a stmt of a 'do' block:
    case j of {
      Left err -> liftIO $ putStrLn err
      Right j -> config j }

Here is config.json

{
  "config":
  {
    "connections": [
    {
      "cfgProperty":
      {
        "Env": "local",
        "Host": "localhost",
        "Port": "8001",
        "Directory": "/apps/workspace/hade/maps/src01_cvs"
      },
      "connectionName": "src01_cvs"
    },
    {
        "cfgProperty":
        {
          "Env": "local",
          "Host": "localhost",
          "Port": "8001",
          "Directory": "/apps/workspace/hade/maps/trg01_cvs"
        },
        "connectionName": "trg01_cvs"
      }
    ]
  }
}{
  "config":
  {
    "connections": [
    {
      "cfgProperty":
      {
        "Env": "local",
        "Host": "localhost",
        "Port": "8001",
        "Directory": "/apps/workspace/hade/maps/src01_cvs"
      },
      "connectionName": "src01_cvs"
    },
    {
        "cfgProperty":
        {
          "Env": "local",
          "Host": "localhost",
          "Port": "8001",
          "Directory": "/apps/workspace/hade/maps/trg01_cvs"
        },
        "connectionName": "trg01_cvs"
      }
    ]
  }
}

I have tried many different configurations using both eitherDecode and decode, but I have run into roadblocks each time. I can get the code to print out the Config record if I change the case to be:

      case j of
        Left err ->  putStrLn err
        Right j -> print $ config j

(along with some other changes), but I cannot get it to simply return the Config record itself. Any assistance would be appreciated.


Solution

  • The root of the problem that you are running into is the call to getCfg in parseCfg. The type of getCfg is IO ByteString which is an IO monad and the only things that you can do with an IO monad are to call the functions that must be defined for all monads (bind, fmap, ap, ...) all of which return another IO monad. This means that if parseCfg is going to call getCfg then it must return an IO monad.

    To quote the haskell wiki : "Because you can't escape from the IO monad, it is impossible to write a function that does a computation in the IO monad but whose result type does not include the IO type constructor."

    One way around this is to call getCfg outside of parseCfg and pass the result to parseCfg. Then parseCfg could return a Config but it makes more sense to return Either String Config so that any parsing error from eitherDecode is preserved. This lets you define parseCfg as nothing more than an fmap of config over the eitherDecode return value.

    The resulting function looks like this:

    parseCfg :: ByteString -> Either String Config
    parseCfg json = config <$> eitherDecode json 
    

    Here is your program, modified to run with the above parseCfg.

    {-# LANGUAGE DeriveGeneric #-}
    {-# LANGUAGE TemplateHaskell #-}
    
    import Data.ByteString.Lazy as B
    import Data.Aeson 
    import Data.Aeson.TH
    import GHC.Generics
    import Control.Monad.IO.Class
    
    data CfgProperty = CfgProperty { 
                           env             :: String,
                           host            :: String,
                           port            :: String,
                           directory       :: String
                       } deriving (Show, Generic)
    
    -- Instead of FromJSON, use deriveJSON for CfgProperty 
    -- which allows changing of field labels from lower 
    -- case to upper case.
    
    $(deriveJSON 
    
          defaultOptions { fieldLabelModifier = let f "env" = "Env"
                                                    f "host" = "Host"
                                                    f "port" = "Port"
                                                    f "directory" = "Directory"
                                                    f other = other
                                                 in f
                         } 
    
          ''CfgProperty
     )
    
    data Connections = Connections { cfgProperty  :: CfgProperty
                       , connectionName :: String
                       } deriving (Show, Generic)
    
    data Config = Config {connections :: [Connections]} deriving (Show, Generic)
    
    data Cfg = Cfg { config :: Config } deriving (Show, Generic)
    
    instance FromJSON Cfg
    instance FromJSON Config
    instance FromJSON Connections
    
    jsonFile :: FilePath
    jsonFile = "config/config.json"
    
    getCfg :: IO ByteString
    getCfg = B.readFile jsonFile
    
    parseCfg :: ByteString -> Either String Config
    parseCfg json = config <$> eitherDecode json 
    
    main :: IO()
    main = do
        json <- getCfg
        case parseCfg json of
            Left err ->  Prelude.putStrLn err
            Right cfg -> print cfg  -- do whatever you want with cfg here.
    

    Here's a modified config.json. The original contained two config JSON records with nothing between them which was not valid JSON, and was not pertinent to the question.

    {
      "config":
      {
        "connections": [
        {
          "cfgProperty":
          {
            "Env": "local",
            "Host": "localhost",
            "Port": "8001",
            "Directory": "/apps/workspace/hade/maps/src01_cvs"
          },
          "connectionName": "src01_cvs"
        },
        {
            "cfgProperty":
            {
              "Env": "local",
              "Host": "localhost",
              "Port": "8001",
              "Directory": "/apps/workspace/hade/maps/trg01_cvs"
            },
            "connectionName": "trg01_cvs"
          }
        ]
      }
    }