如何使用 Aeson 解析分布在数组中的值?

How to parse values distributed across an array with Aeson?

我的 json 值为:

{
  "name": "xyz1",
  "extra": [
    {
      "this_string_A": "Hello"
    },
    {
      "this_string_B": "World"
    }
  ]
}

数据类型为:

data Abc = Abc
  { name :: String 
  , a :: Maybe String
  , b :: Maybe String
  } deriving (Generic, Show)

在上述情况下,我希望它以 Abc "xyz1" (Just "Hello") (Just "World").

的结果进行解析

我不知道如何在 aeson [=18] 中有条件地解析 extra 中的值(这是一个 JSON array) =] 语境。例如,我怎样才能得到 extra[0].this_string_a?我

我试过的:

我以为我可以创建自己的 Parser (Maybe String) 函数,但 运行 陷入混乱的错误:

instance FromJSON Abc where
     parseJSON = withObject "Abc" $ \v -> Abc
         <$> v .: "name"
         <*> myParse v
         <*> myParse v

myParse :: Object -> Parser (Maybe String)
myParse x =  withArray "extra" myParse2 (x)

myParse2 :: Array -> Parser (Maybe String)
myParse2 = undefined

类型检查失败:

    • Couldn't match type ‘unordered-containers-0.2.10.0:Data.HashMap.Base.HashMap
                             text-1.2.3.1:Data.Text.Internal.Text Value’
                     with ‘Value’
      Expected type: Value
        Actual type: Object
    • In the third argument of ‘withArray’, namely ‘(x)’

如果我用 Object x 替换 x 然后我得到解析错误:

Left "Error in $: parsing extra failed, expected Array, but encountered Object" 

完整示例(运行 test 函数测试):

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
module Example where

import GHC.Generics
import Data.Aeson
import Data.Aeson.Types

data Abc = Abc
  { name :: String 
  , a :: Maybe String
  , b :: Maybe String
  } deriving (Generic, Show)

instance FromJSON Abc where
     parseJSON = withObject "Abc" $ \v -> Abc
         <$> v .: "name"
         <*> (v.: "extra") -- find where object has key of this_string_a ??
         <*> (v.: "extra") -- find where object has key of this_string_b ??

test :: Either String Abc
test = eitherDecode exampleJson

exampleJson = "{ \"name\": \"xyz1\", \"extra\": [ { \"this_string_A\": \"Hello\" }, { \"this_string_B\": \"World\" } ] }"

部分回答...

instance FromJSON Abc where
     parseJSON = withObject "Abc" $ \v -> Abc
         <$> v .: "name"
         <*> (v .: "extra" >>= myParse)
         <*> (v .: "extra" >>= myParse)


myParse :: Array -> Parser (Maybe String)
myParse x = withArray "extra" (lookupDictArray "this_string_a") (Array x)

lookupDictArray :: Text -> Array -> Parser (Maybe String)
lookupDictArray k a = do
  let v = Vector.find (maybe False (HashMap.member k) . parseMaybe parseJSON) a
  case v of
    Just v' -> withObject "grrrrrrrrrrr" (\v -> v .: k) v'
    Nothing -> pure Nothing

类型检查失败:

src/Example.hs:32:69-77: error:
    • Ambiguous type variable ‘a0’ arising from a use of 
‘parseJSON’
      prevents the constraint ‘(FromJSON a0)’ from being 
solved.
      Probable fix: use a type annotation to specify 
what ‘a0’ should be.
      These potential instances exist:
        instance FromJSON DotNetTime
          -- Defined in ‘aeson-1.4.4.0:Data.Aeson.Types.FromJSON’
        instance FromJSON Value
          -- Defined in ‘aeson-1.4.4.0:Data.Aeson.Types.FromJSON’
        instance (FromJSON a, FromJSON b) => FromJSON 
(Either a b)
          -- Defined in ‘aeson-1.4.4.0:Data.Aeson.Types.FromJSON’
        ...plus 29 others
        ...plus 60 instances involving out-of-scope types
        (use -fprint-potential-instances to see them all)
    • In the first argument of ‘parseMaybe’, namely 
‘parseJSON’
      In the second argument of ‘(.)’, namely 
‘parseMaybe parseJSON’
      In the first argument of ‘Vector.find’, namely
        ‘(maybe False (member k) . parseMaybe 
parseJSON)’
   |
32 |   let v = (Vector.find (maybe False (HashMap.member 
k) . parseMaybe parseJSON) a)

withXXX“帮手”让一切都变得有点尴尬,但就是这样。

Aeson Parser 类型命名错误,这会造成混淆。 Aeson Parser 对象的想法是它们代表一个单子解析 result。 (这不同于您在 Parsec 等中找到的 Parser 对象,它们代表实际的单子解析器。)因此,您应该将 Parser a 视为与 Either ParseError a 同构——具有失败可能性的单子结果。

这些解析结果通常以应用的方式组合。所以如果你有一个像这样的解析器:

data Xyz = Xyz { x :: String, y :: String }
instance FromJSON Xyz where
  parseJSON = withObject "Xyz" $ \v ->
    Xyz <$> v .: "x" <*> v .: "y"

解析结果 v .: "x"v .: "y" 的类型 Parser StringEither ParseError a 非常相似,该实例的最后一行是组合成功的常用方法和不成功的结果以适用的方式,沿着:

Xyz <$> Right "value_x" <*> Left "while parsing Xyz: key y was missing"

现在,函数 parseJSON 的类型为 Value -> Parser a。这应该被称为 parser,但为了避免混淆,我们称它为“解析函数”。解析函数采用 JSON 表示(ValueObject 或其他一些 JSON 东西)和 return 解析结果。 withXXX 函数族用于在 JSON 事物之间调整解析函数。如果您有一个需要 Object 的解析函数,例如:

\v -> Xyz <$> v .: "x" <*> v .: "y"   :: Object -> Parser Xyz

而你想把它改编成parseJSON :: Value -> Parser Xyz,你用withObject "str" :: (Object -> Parser Xyz) -> (Value -> Parser Xyz)来做。

回到你的问题上来,如果你想编写一个如下所示的核心解析器:

\v -> Abc <$> v .: "name" <*> extra .:? "this_string_A"
                          <*> extra .:? "this_string_B"

您希望 extra 成为一个 Object,并且您希望使用适当的 withXXX 从整个 JSON 对象 v :: Object 中单子提取它帮助程序将解析函数从一种输入 JSON thingy 类型调整为另一种。所以,让我们写一个 monadic 函数(实际上是一个解析函数)来做到这一点:

getExtra :: Object -> Parser Object
getExtra v = do

首先,我们从 v 中单子提取可选的“额外”组件。我们这里使用条件形式,所以mextra :: Maybe Value.

  mextra <- v .:? "extra"

其次,让我们从“mextra”中单子创建最终的 Object。这将是 JSON Object,其键为 "this_string_A""this_string_B",删除了数组层。请注意,此 case 表达式的类型将为 Parser Object,类型为 Object = HashMap key value 的解析结果。对于 Just 的情况,我们有一个 Value 我们希望它是一个数组,所以让我们使用 withArray 助手来确保这一点。请注意,withArray "str" 辅助函数采用 \arr -> do ... :: Array -> Parser Object 类型的 解析函数 并将其调整为 Value -> Parser Object,因此它可以应用于 vv :: Value.

  case mextra of
    Just vv -> vv & withArray "Abc.extra" (\arr -> do

现在,arr 是一个 Array = Vector Value。我们希望它是一个 Object 的数组。让我们把 Values 作为一个列表拉出来:

      let vallst = toList arr

然后在 withObject 的帮助下单子遍历列表以确保它们都是预期的 Object。注意这里使用了 pure,因为我们想在不进行任何额外处理的情况下按原样提取 Object

      objlst <- traverse (withObject "Abc.extra[..]" pure) vallst

现在,我们有一个 objlst :: [Object]。它们是一组具有不相交键的单例 hashmap,我们想要的 Object / hashmap 是它们的并集,所以让我们 return 那样。此处的括号结束了应用于 vv:

withArray 表达式
      return $ HashMap.unions objlst)

对于 Nothing 的情况(未找到“额外的”),我们只是 return 一个空的 hashmap:

    Nothing -> return HashMap.empty

完整的函数如下所示:

getExtra :: Object -> Parser Object
getExtra v = do
  mextra <- v .:? "extra"
  case mextra of
    Just vv -> vv & withArray "Abc.extra" (\arr -> do
      let vallst = toList arr
      objlst <- traverse (withObject "Abc.extra[..]" pure) vallst
      return $ HashMap.unions objlst)
    Nothing -> return HashMap.empty

然后你在你的解析器实例中像这样使用它:

instance FromJSON Abc where
  parseJSON =
   withObject "Abc" $ \v -> do
    extra <- getExtra v
    Abc <$> v .: "name" <*> extra .:? "this_string_A" <*> extra .:? "this_string_B"

有测试用例:

example :: BL.ByteString
example = "{\"name\": \"xyz1\", \"extra\": [{\"this_string_A\": \"Hello\"}, {\"this_string_B\": \"World\"}]}"

main = print (eitherDecode example :: Either String Abc)

它是这样工作的:

λ> main
Right (Abc {name = "xyz1", a = Just "Hello", b = Just "World"})

完整代码:

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}

import Data.Aeson (eitherDecode, FromJSON, Object, parseJSON, withArray, withObject, (.:), (.:?))
import Data.Aeson.Types (Parser)
import GHC.Generics (Generic)
import qualified Data.ByteString.Lazy as BL (ByteString)
import qualified Data.HashMap.Strict as HashMap (empty, unions)
import Data.Function ((&))
import Data.Foldable (toList)

data Abc = Abc
  { name :: String
  , a :: Maybe String
  , b :: Maybe String
  } deriving (Generic, Show)

instance FromJSON Abc where
  parseJSON =
   withObject "Abc" $ \v -> do
    extra <- getExtra v
    Abc <$> v .: "name" <*> extra .:? "this_string_A" <*> extra .:? "this_string_B"

getExtra :: Object -> Parser Object
getExtra v = do
  mextra <- v .:? "extra"
  case mextra of
    Just vv -> vv & withArray "Abc.extra" (\arr -> do
      let vallst = toList arr
      objlst <- traverse (withObject "Abc.extra[..]" pure) vallst
      return $ HashMap.unions objlst)
    Nothing -> return HashMap.empty

example :: BL.ByteString
example = "{\"name\": \"xyz1\", \"extra\": [{\"this_string_A\": \"Hello\"}, {\"this_string_B\": \"World\"}]}"

main = print (eitherDecode example :: Either String Abc)