4

I'm currently struggling to parse some JSON data using the aeson library. There are a number of properties that have the value false when the data for that property is absent. So if the property's value is typically an array of integers and there happens to be no data for that property, instead of providing an empty array or null, the value is false. (The way that this data is structured isn't my doing so I'll have to work with it somehow.)

Ideally, I would like to end up with an empty list in cases where the value is a boolean. I've created a small test case below for demonstration. Because my Group data constructor expects a list, it fails to parse when it encounters false.

  data Group = Group [Int] deriving (Eq, Show)

  jsonData1 :: ByteString
  jsonData1 = [r|
    {
      "group" : [1, 2, 4]
    }
  |]

  jsonData2 :: ByteString
  jsonData2 = [r|
    {
      "group" : false
    }
  |]

  instance FromJSON Group where
    parseJSON = withObject "group" $ \g -> do
      items <- g .:? "group" .!= []
      return $ Group items

  test1 :: Either String Group
  test1 = eitherDecode jsonData1
  -- returns "Right (Group [1,2,4])"

  test2 :: Either String Group
  test2 = eitherDecode jsonData2
  -- returns "Left \"Error in $.group: expected [a], encountered Boolean\""

I was initially hoping that the (.!=) operator would allow it to default to an empty list but that only works if the property is absent altogether or null. If it were "group": null, it would parse successfully and I would get Right (Group []).

Any advice for how to get it to successfully parse and return an empty list in these cases where it's false?

Chris Stryczynski
  • 30,145
  • 48
  • 175
  • 286

1 Answers1

2

One way to solve this problem is to pattern match on the JSON data constructors that are valid for your dataset and raise invalid for all others.

For instance, you could write something like this for that particular field, keeping in mind that parseJSON is a function from Value -> Parser a:

instance FromJSON Group where
    parseJSON (Bool False) = Group <$> pure []
    parseJSON (Array arr) =  pure (Group $ parseListOfInt arr)
    parseJSON invalid    = typeMismatch "Group" invalid

parseListOfInt :: Vector Value -> [Int]
parseListOfInt = undefined -- build this function

You can see an example of this in the Aeson docs, which are pretty good (but you kind of have to read them closely and a few times through).

I would probably then define a separate record to represent the top-level object that this key comes in and rely on generic deriving, but others may have a better suggestion there:

data GroupObj = GroupObj { group :: Group } deriving (Eq, Show)
instance FromJSON GroupObj

One thing to always keep in mind when working with Aeson are the core constructors (of which there are only 6) and the underlying data structures (HashMap for Object and Vector for Array, for instance).

For example, in the above, when you pattern match on Array arr, you have to be aware that you're getting a Vector Value there in arr and we still have some work to do to turn this into a list of integers, which is why I left that other function parseListOfInt undefined up above because I think it's probably a good exercise to build it?

erewok
  • 7,555
  • 3
  • 33
  • 45