Data.Aeson 中的模式匹配向量值

Pattern Match Vector Value in Data.Aeson

我正在使用 Data.Aeson 将 JSON 解析为我的自定义类型。 我尝试在 FromJSON 实例中进行模式匹配 Vector Value (Array),但不知道该怎么做。 JSON value 键值可以是 StringString 列表或 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

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

我可以在 Array 上使用模式匹配来处理这种情况吗?还是我应该手动检查每个 Value 的类型?怎么办?

不,您不能按照您在此处尝试执行的方式进行模式匹配。 JSON 数组可以包含不同类型的值,并且您不能像在一个列表中那样对列表中的所有值进行模式匹配。

这里有几种方法可以解决您的实际问题。有一种简单的方法,还有一种显式的方法可以给你更好的错误信息。

简单的方法

最简单的方法是利用 Text[a] 已经存在 FromJSON 个实例这一事实。因此,您可以使用 Alternative 运算符来编写您的实例,如下所示:

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

这里的技巧是 Aeson 首先会尝试解析一个 Text 值,然后如果失败它会尝试 [Text],如果再次失败它会尝试 [[Text]] .

此解决方案的问题在于,如果您的 JSON 格式不正确,则错误消息可能没有意义。例如,如果你给它一个顶级 Null 值,你的错误将是它期望一个 [[Text]],因为你总是会得到链中最后一个值的错误。

显式方式

要获得更好的错误消息,您必须更明确地说明您期望的值。如果结果是一个空数组,应该是 ListOfText 还是 ListOfListOfText?由于我们不能直接在 Vector 上进行模式匹配,我们可以将其转换为列表并在其上进行模式匹配:

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"