Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to parse a JSON string using Aeson that can be one of two different types

Tags:

haskell

aeson

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?

like image 239
imanschumpeter Avatar asked Oct 17 '25 12:10

imanschumpeter


1 Answers

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?

like image 152
erewok Avatar answered Oct 21 '25 12:10

erewok



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!