Haskell Aeson how to decode / parse based on `Value` type rather than `ByteString`?

Viewed 672

I have the following function:

parseUserBasic :: ByteString -> Either String [Either String UserBasic]
parseUserBasic x = do 
  xx <- parseItems x
  pure $ fmap (eitherDecode . encode) (items xx)

However it's not very efficient because of pure $ fmap (eitherDecode . encode) (items xx) - we are JSON encoding a Object into a ByteString, which we then decode into a UserBasic. Is there a way to do this directly? I assume this function would have the type FromJSON a => Value -> Maybe a.

The complete code:

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

module UserBasic where

import GHC.Generics
import Data.Aeson
import Data.String.Conversions
import Data.ByteString.Lazy

data UserBasic = UserBasic
  { email :: String
  , name :: String
  , address_1 :: Maybe String
  , address_2 :: Maybe String
  , address_3 :: Maybe String
  , address_4 :: Maybe String
  } deriving (Generic, Show)

data Items = Items
  { items :: [Object]
  } deriving (Generic, Show)

instance FromJSON Items where
instance ToJSON Items where
  toEncoding = genericToEncoding defaultOptions

instance FromJSON UserBasic where
     parseJSON = withObject "Person" $ \v -> UserBasic
         <$> v .: "email"
         <*> ((++) . (++ " ") <$> v .: "first_name" <*> v .: "last_name")
         <*> v .: "first_name"
         <*> v .: "first_name"
         <*> v .: "first_name"
         <*> v .: "first_name"

parseItems :: ByteString -> Either String Items
parseItems = eitherDecode

parseUserBasic :: ByteString -> Either String [Either String UserBasic]
parseUserBasic x = do 
  xx <- parseItems x
  pure $ fmap (eitherDecode . encode) (items xx)
2 Answers

I recommend the following abbreviated code:

newtype Items = Items
  { items :: [UserBasic]
  } deriving (Generic, Show)

instance FromJSON Items
instance ToJSON Items where
  toEncoding = genericToEncoding defaultOptions

parseUserBasic :: ByteString -> Either String [UserBasic]
parseUserBasic bs = items <$> eitherDecode bs

-- OR, even better in many cases, don't bother unwrapping Items:

parseItems :: ByteString -> Either String Items
parseItems = eitherDecode -- why even bother naming parseItems lmao

Don't first decode to Object, then do further decoding; just decode directly to the type you care about.

Thanks to @snak pointing out fromJSON which is defined as:

-- | Convert a value from JSON, failing if the types do not match.
fromJSON :: (FromJSON a) => Value -> Result a
fromJSON = parse parseJSON

Which lead me to the solution:

fff :: Value -> Either String UserBasic
fff x = parseEither parseJSON $ x

parseUserBasic :: ByteString -> Either String [Either String UserBasic]
parseUserBasic x = do 
  xx <- parseItems x
  pure $ fmap (fff) (fmap Object $ items xx)
Related