11

我正在使用一个返回 JSON 响应的外部 API。其中一个响应是对象数组,这些对象由其中的字段值标识。我在理解如何使用 Aeson 解析这种 JSON 响应时遇到了一些麻烦。

这是我的问题的简化版本:

newtype Content = Content { content :: [Media] } deriving (Generic)

instance FromJSON Content

data Media =
  Video { objectClass :: Text
        , title :: Text } |
  AudioBook { objectClass :: Text
            , title :: Text }

在 API 文档中,据说可以通过字段objectClass来识别对象,该字段对于我们的Video对象具有值“video”,对于我们的AudioBook等具有值“audiobook” 。示例 JSON:

[{objectClass: "video", title: "Some title"}
,{objectClass: "audiobook", title: "Other title"}]

问题是如何使用 Aeson 处理这种类型的 JSON?

instance FromJSON Media where
  parseJSON (Object x) = ???
4

2 回答 2

11

你基本上需要一个功能Text -> Text -> Media

toMedia :: Text -> Text -> Media
toMedia "video"     = Video "video"
toMedia "audiobook" = AudioBook "audiobook"

FromJSON实例现在非常简单(使用<$>and <*>from Control.Applicative):

instance FromJSON Media where
    parseJSON (Object x) = toMedia <$> x .: "objectClass" <*> x .: "title"

但是,此时您是多余的:or中的objectClass字段不会为您提供比实际类型更多的信息,因此您可以将其删除:VideoAudio

data Media = Video     { title :: Text }
           | AudioBook { title :: Text }

toMedia :: Text -> Text -> Media
toMedia "video"     = Video
toMedia "audiobook" = AudioBook

另请注意,这toMedia是部分的。您可能想要捕获无效"objectClass"值:

instance FromJSON Media where
    parseJSON (Object x) = 
        do oc <- x .: "objectClass"
           case oc of
               String "video"     -> Video     <$> x .: "title"
               String "audiobook" -> AudioBook <$> x .: "title"
               _                  -> empty

{- an alternative using a proper toMedia
toMedia :: Alternative f => Text -> f (Text -> Media)
toMedia "video"     = pure Video
toMedia "audiobook" = pure AudioBook
toMedia _           = empty

instance FromJSON Media where
    parseJSON (Object x) = (x .: "objectClass" >>= toMedia) <*> x .: "title"
-}

最后但同样重要的是,请记住有效的 JSON使用字符串作为名称。

于 2014-08-26T08:28:37.013 回答
5

数据类型的默认翻译,例如:

data Media = Video     { title :: Text }
           | AudioBook { title :: Text }
             deriving Generic

实际上非常接近你想要的。(为了示例的简单性,我定义ToJSON了实例并对示例进行编码,以查看我们得到的 JSON 类型。)

aeson,默认

因此,使用我们拥有的默认实例(查看产生此输出的完整源文件):

[{"tag":"Video","title":"Some title"},{"tag":"AudioBook","title":"Other title"}]

让我们看看我们是否可以通过自定义选项更接近...

aeson, 定制tagFieldName

使用自定义选项

mediaJSONOptions :: Options
mediaJSONOptions = 
    defaultOptions{ sumEncoding = 
                        TaggedObject{ tagFieldName = "objectClass"
                                    -- , contentsFieldName = undefined
                                    }
                  }

instance ToJSON Media
    where toJSON = genericToJSON mediaJSONOptions

我们得到:

[{"objectClass":"Video","title":"Some title"},{"objectClass":"AudioBook","title":"Other title"}]

(想想你自己想对实际代码中的未定义字段做什么。)

aeson, 定制constructorTagModifier

添加

              , constructorTagModifier = fmap Char.toLower

mediaJSONOptions

[{"objectClass":"video","title":"Some title"},{"objectClass":"audiobook","title":"Other title"}]

伟大的!正是你指定的!

解码

只需添加一个具有相同选项的实例即可从此格式解码:

instance FromJSON Media
    where parseJSON = genericParseJSON mediaJSONOptions

例子:

*Main> encode example
"[{\"objectClass\":\"video\",\"title\":\"Some title\"},{\"objectClass\":\"audiobook\",\"title\":\"Other title\"}]"
*Main> decode $ fromString "[{\"objectClass\":\"video\",\"title\":\"Some title\"},{\"objectClass\":\"audiobook\",\"title\":\"Other title\"}]" :: Maybe [Media]
Just [Video {title = "Some title"},AudioBook {title = "Other title"}]
*Main>

完整的源文件

通用 aeson,默认

为了获得更完整的画面,让我们也看看generic-aeson会提供什么包(在hackage)。它也有很好的默认翻译,在某些方面与aeson.

正在做

import Generics.Generic.Aeson -- from generic-aeson package

并定义:

instance ToJSON Media
    where toJSON = gtoJson

给出结果:

[{"video":{"title":"Some title"}},{"audioBook":{"title":"Other title"}}]

因此,它与我们在使用aeson.

generic-aeson 的选项(设置)对我们来说并不有趣(它们只允许去除前缀)。

完整的源文件。)

aeson,ObjectWithSingleField

除了小写构造函数名称的第一个字母之外,generic-aeson的翻译似乎类似于 中可用的选项aeson

让我们试试这个:

mediaJSONOptions = 
    defaultOptions{ sumEncoding = ObjectWithSingleField
                  , constructorTagModifier = fmap Char.toLower
                  }

是的,结果是:

[{"video":{"title":"Some title"}},{"audiobook":{"title":"Other title"}}]

其余选项:(aeson, TwoElemArray)

上面没有考虑一个可用的选项,因为sumEncoding它提供了一个与所询问的 JSON 表示不太相似的数组。是TwoElemArray。例子:

[["video",{"title":"Some title"}],["audiobook",{"title":"Other title"}]]

是(谁)给的:

mediaJSONOptions = 
    defaultOptions{ sumEncoding = TwoElemArray
                  , constructorTagModifier = fmap Char.toLower
                  }
于 2015-03-26T18:31:22.683 回答