解析一个 aeson 对象的 "the rest"

Parsing "the rest" of an aeson object

出于某种原因,我无法全神贯注于 Aeson 中任意成功的解析,而不会使整个系统崩溃并导致 space 泄漏。

这是我的问题:

newtype Foo = Foo
  { getFoo :: [(String, Maybe String)]
  } deriving (Show, Eq)

instance ToJSON Foo where
  toJSON (Foo xs) = object $
    map (\(k,mv) -> T.pack k .= mv) xs

到目前为止,对 Foo 进行编码非常好。但是,我想制作一个 拒绝 几个键的解析器,如果它们存在的话。现在,我正在进行伪拒绝,这就是为什么我认为我得到了一个糟糕的结果:

import qualified Data.HashMap as HM

-- the "duck-tape and chewing gum" approach
instance FromJSON Foo where
  parseJSON (Object o) = Foo <$> parseJSON (Object theRest)
    where
      theRest = foldr HM.delete o [ "foo"
                                  , "bar"
                                  ]
  parseJSON _ = empty

这个版本让我认为操作内部对象是不正确的,因为解析器可能在解析器之外的 HashMap 中获取 "more" 数据(因为惰性字节串被输入它),但我显然不确定这一点。所以,我尝试了一种不同的方法:

instance FromJSON Foo where
  parseJSON (Object o) =
    (Foo . filter (\(k,_) -> k `elem` toIgnore)) <$>
      parseJSON (Object o)
    where
      toIgnore = ["foo", "bar"]
  parseJSON _ = empty

但是这个 似乎会导致死锁/space 泄漏(不确定要诊断这种执行停止的确切原因)。除了对象的几个键之外,什么是接受所有的建议方法?我需要在 (Object o) 结构上进行模式匹配,因为我正在为我的数据类型在不同的组件中手动查找 o .: "foo"o .: "bar"。理想情况下,我只想 从内容中删除 那些键并继续解析,因为我 已经考虑了它们 (因此 - "the rest").

还有希望吗?

对于您的 PartialAppContact 示例,这里有一个似乎有效的更普通的方法:

{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}

import Data.Aeson
import qualified Data.Text as T
import qualified Data.HashMap.Strict  as HM
import Control.Monad
import Text.Heredoc

type RequiredField = String
type OptionalField = String

data PartialAppContact = PartialAppContact
  { partialAppContactRequired :: [(RequiredField, String)]
  , partialAppContactOptional :: [(OptionalField, Maybe String)]
  } deriving (Show, Eq)

instance FromJSON PartialAppContact where
  parseJSON (Object o) = do
    let required = [ "firstName", "lastName", "email", "phoneNumber" ]
    reqPairs <- forM required $ \k -> do
      v <- o .: k
      s <- parseJSON v
      return (T.unpack k, s)
    nonReqPairs <- forM [ (k,v) | (k,v) <- HM.toList o, k `notElem` required ] $ \(k,v) -> do
      s <- parseJSON v
      return (T.unpack k, s)
    return $ PartialAppContact reqPairs nonReqPairs

test1 = Data.Aeson.eitherDecode "{\"firstName\":\"Athan'\"}" :: Either String PartialAppContact

input = [str|
| { "firstName": "a first name"
| , "lastName": "a last name"
| , "email": "asasd@asd.com"
| , "phoneNumber": "123-123-123"
| , "another field": "blah blah" }
|]

test2 = Data.Aeson.eitherDecode "{\"firstName\":\"Athan'\" }" :: Either String PartialAppContact

test3 = Data.Aeson.eitherDecode input :: Either String PartialAppContact

更新

根据您的意见,考虑编写实例的思路:

import Data.List (partition)

instance FromJSON PartialAppContact where
  parseJSON (Object o) = do
    let required = [ "firstName", "lastName", "email", "phoneNumber" ]
    let (group1, group2) = partition (\(k,_) -> k `elem` required) (HM.toList o)
    reqFields   <- forM group1 $ \(k,v) -> do s <- parseJSON v; return (T.unpack k, s)
    otherFields <- forM group2 (\(k,v) -> (T.unpack k,) <$> parseJSON v)
    return $ PartialAppContact reqFields otherFields

我发现一个有效的实现需要使用 (.:?),才能正确实现 可选的已知 字段。从那里,您可以自由分解 HashMap 并重新 parseJSON 它的子字段:

instance FromJSON Foo where
  parseJSON (Object o) = do
    mfoo <- o .:? "foo"
    mbar <- o .:? "bar"
    let foundFields = catMaybes [mfoo, mbar]
    rest <- mapM (\(k,v) -> (T.unpack k,) <$> parseJSON v) 
              (toList theRest)
    return $ Foo rest -- assuming you're done with `foundFields`
    where
      theRest = foldr HM.delete o ["foo", "bar"]

要查看评论中讨论的问题的最终实现,see this commit