Pattern Match Vector Value in Data.Aeson

545 views Asked by At

I am using Data.Aeson to parse JSON to my custom type. I try to pattern match Vector Value (Array) in my FromJSON instance, but don't know how I can do it. JSON value key can have a value of a String, a list of String or a list of list of String.

instance FromJSON Foo where
  parseJSON (Object o) =
    case lookup "value" o of
      Just (String s) -> pure $ SimpleText s
      Just foo@(Array (String s)) -> pure $ ListOfText $ V.toList <$> V.mapM (parseJSON :: Value -> Parser Text) foo
      Just foo@(Array (Array (String s))) -> pure $ ListOfListOfText $ V.toList <$> V.mapM (parseJSON :: Value -> Parser Text) $ V.toList <$> V.mapM (parseJSON :: Value -> [Parser Value]) foo

with

data Foo = SimpleText Text
         | ListOfText [Text]
         | ListOfListOfText [[Text]]
         deriving (Show, Eq, Ord)

Can I use pattern matching on Array to handle this case? or should I manually check the type of every Value? and how do to it?

1

There are 1 answers

1
Reite On BEST ANSWER

No, you cannot pattern match the way you are trying to do here. A JSON Array can contain values of different types, and you cannot pattern match on all values in a list like they where one.

There are several ways to solve your actual problem here. There is a easy way, and there is an explicit way that will give you better error messages.

Easy way

The easy way is to use the fact that there already exist FromJSON instances for Text and [a]. Because of this you can use the Alternative operator to write your instance like this:

instance FromJSON Foo where
    parseJSON v =  (SimpleText <$> parseJSON v) 
               <|> (ListOfText <$> parseJSON v) 
               <|> (ListOfListOfText <$> parseJSON v)

The trick here is that Aeson first will try to parse a Text value, then if it fails it will try a [Text], if it fails again it will try [[Text]].

The problem with this solution is that if your JSON is malformed the error messages might not make sense. For example if you give it a top level Null value your error will be that it expects a [[Text]], since you will always get the error for the last value in the chain.

Explicit way

To get better error messages you have to be more expicit about what values you expect. What if the result is an empty array, should it be a ListOfText or a ListOfListOfText? Since we cant pattern match on a Vector directly, we can turn it into a list and pattern match on it:

instance FromJSON Foo where
    parseJSON v = case v of
        -- If its a string, we return the string as a SimpleText
        (String s) -> return $ SimpleText s

        -- If its an array, we turn the vector to a list so we can pattern match on it
        (Array a)  -> case V.toList a of

            -- If its a empty list, we return a empty ListOfText
            []             -> return $ ListOfText []

            -- If the first value is a string, we put it as the first element of our ListOfTexts and try to parse the rest.
            (String s: xs) -> ListOfText . (s:) <$> mapM parseJSON xs

            -- If the first value is an array, we try to parse it as [Text], then parse the rest.
            (Array a: xa)  -> ListOfListOfText <$> ((:) <$> parseJSON (Array a) <*> mapM parseJSON xa)

            -- If the first value is neither a string or array we return a error message.
            _              -> fail "Expected an Array or an Array of Arrays."

        -- If the top level value is not a string or array we return a error message.
        _ -> fail "Expected a String or Array"