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

时间:2014-08-26 08:03:25

标签: json parsing haskell aeson

我使用的是返回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"我们的视频对象和#34;有声读物"我们的 AudioBook 等等。示例JSON:

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

问题是如何使用Aeson来接近这种类型的JSON?

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

2 个答案:

答案 0 :(得分:9)

你基本上需要一个函数Text -> Text -> Media

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

FromJSON实例现在非常简单(使用来自<$>的{​​{1}}和<*>):

Control.Applicative

但是,此时您还是多余的:instance FromJSON Media where parseJSON (Object x) = toMedia <$> x .: "objectClass" <*> x .: "title" objectClass中的Video字段并未提供比实际类型更多的信息,因此您可能会删除它:

Audio

另请注意,data Media = Video { title :: Text } | AudioBook { title :: Text } toMedia :: Text -> Text -> Media toMedia "video" = Video toMedia "audiobook" = AudioBook 是部分的。您可能希望捕获无效的toMedia值:

"objectClass"

最后,但并非最不重要的是,请记住valid JSON使用字符串作为名称。

答案 1 :(得分:3)

数据类型的默认转换,如:

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

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

aeson,默认

因此,使用默认实例(查看the complete source file生成此输出):

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

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

aeson,custom tagFieldName

使用自定义options

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,custom 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>

Complete source file

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的选项(Settings)对我们来说并不感兴趣(它们只允许删除前缀)。

The complete source file。)

aeson,ObjectWithSingleField

除了下限 - 构造函数名称的第一个字母外,generic-aeson的翻译类似于aeson中提供的选项:

让我们试试这个:

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

是的,结果是:

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

其他选项:(aeson,TwoElemArray

上面考虑了一个sumEncoding TwoElemArray可用的option,因为它给出了一个与所询问的JSON表示不太相似的数组。它是[["video",{"title":"Some title"}],["audiobook",{"title":"Other title"}]] 。例如:

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

由:

给出
{{1}}