Search code examples
jsonparsinghaskellaeson

Parsing nested JSON into a list of Tuples with Aeson


Say I have the following structure:

data AddressDto = AddressDto
  { addressDtoId                   :: UUID 
  , addressDtoCode                 :: Text
  , addressDtoCity                 :: Maybe Text 
  , addressDtoStreet               :: Text 
  , addressDtoPostCode             :: Text
  } deriving (Eq, Show, Generic)

instance FromJSON AddressDto where
  parseJSON = genericParseJSON $ apiOptions "addressDto"

instance ToJSON AddressDto where
  toJSON = genericToJSON $ apiOptions "addressDto"
  toEncoding = genericToEncoding $ apiOptions "addressDto"

This works as you would expect.

Now say I want to parse a JSON structure of the format:

{ UUID: AddressDto, UUID: AddressDto, UUID: AddressDto }

A reasonable Haskell representation would seem to be:

data AddressListDto = AddressListDto [(UUID, AddressDto)]

Creating a helper function like so:

keyAndValueToList :: Either ServiceError AddressListDto -> Text -> DiscountDto -> Either ServiceError AddressListDto
keyAndValueToList (Left err) _ _ = Left err
keyAndValueToList (Right (AddressListDto ald)) k v = do
  let maybeUUID = fromString $ toS k 
  case maybeUUID of 
    Nothing        -> Left $ ParseError $ InvalidDiscountId k
    Just validUUID -> Right $ AddressListDto $ (validUUID, v) : ald

and finally an instance of FromJSON:

instance FromJSON AddressListDto where
  parseJSON = withObject "tuple" $ \o -> do 
    let result = foldlWithKey' keyAndValueToList (Right $ AddressListDto []) o
    case result of
      Right res -> pure res
      Left err -> throwError err

This fails to compile with:

Couldn't match type ‘aeson-1.4.5.0:Data.Aeson.Types.Internal.Value’
                     with ‘AddressDto’
      Expected type: unordered-containers-0.2.10.0:Data.HashMap.Base.HashMap
                       Text AddressDto

Two questions:

1) How do I make sure the nested values in the hashmap get parsed correctly to an AddressDto. 2) How do I avoid forcing the initial value into an either? Is there a function I could use instead of foldlWithKey' that didn't make me wrap the initial value like this?


Solution

  • Funny answer. To implement parseJSON, you can use

    parseJSON :: Value -> Parser (HashMap Text AddressDto)
    

    ...but even better, why not just use a type alias instead of a fresh data type, so:

    type AddressListDto = HashMap UUID AddressDto
    

    This already has a suitable FromJSON instance, so then you don't even need to write any code yourself.