Haskell :: Aeson :: 根据字段值解析 ADT

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) = ???
2个回答

11

你基本上需要一个函数文本 -> 文本 -> 媒体

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

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

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

然而,此时您是多余的:在VideoAudio中的objectClass字段并没有提供比实际类型更多的信息,因此您可以将其删除:

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使用字符串作为名称。


1
谢谢你指出关于有效的 JSON,我只是在没有查找的情况下输入我的问题,并未提供正确的 JSON 表示。 - ksaveljev
@ksaveljev:没关系,这只是一个提醒,因为在JavaScript中,即使键不是字符串,{ key1: value1, key2: value2, ... }也是有效的。 - Zeta

5

像数据类型这样的默认翻译:

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

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

aeson, default

因此,使用默认实例(请参见完整源文件生成此输出):

[{"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>

完整源文件

generic-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
                  }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接