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

ksa*_*jev 10 parsing json 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 }
Run Code Online (Sandbox Code Playgroud)

在API文档中,可以说对象可以通过字段objectClass来识别,该字段对于我们的Video对象具有值"video"而对于我们的AudioBook具有 "有声读物" 等等.示例JSON:

[{objectClass: "video", title: "Some title"}
,{objectClass: "audiobook", title: "Other title"}]
Run Code Online (Sandbox Code Playgroud)

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

instance FromJSON Media where
  parseJSON (Object x) = ???
Run Code Online (Sandbox Code Playgroud)

Zet*_*eta 9

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

toMedia :: Text -> Text -> Media
toMedia "video"     = Video "video"
toMedia "audiobook" = AudioBook "audiobook"
Run Code Online (Sandbox Code Playgroud)

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

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

但是,此时您是多余的:objectClass字段中Video或者Audio没有提供比实际类型更多的信息,因此您可以将其删除:

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

toMedia :: Text -> Text -> Media
toMedia "video"     = Video
toMedia "audiobook" = AudioBook
Run Code Online (Sandbox Code Playgroud)

另请注意,这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"
-}
Run Code Online (Sandbox Code Playgroud)

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


imz*_*hev 5

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

data Media = Video     { title :: Text }
           | AudioBook { title :: Text }
             deriving Generic
Run Code Online (Sandbox Code Playgroud)

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

AESON,默认

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

[{"tag":"Video","title":"Some title"},{"tag":"AudioBook","title":"Other title"}]
Run Code Online (Sandbox Code Playgroud)

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

AESON, 定制 tagFieldName

使用自定义选项

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

instance ToJSON Media
    where toJSON = genericToJSON mediaJSONOptions
Run Code Online (Sandbox Code Playgroud)

我们得到:

[{"objectClass":"Video","title":"Some title"},{"objectClass":"AudioBook","title":"Other title"}]
Run Code Online (Sandbox Code Playgroud)

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

AESON, 定制 constructorTagModifier

添加

              , constructorTagModifier = fmap Char.toLower
Run Code Online (Sandbox Code Playgroud)

mediaJSONOptions得到:

[{"objectClass":"video","title":"Some title"},{"objectClass":"audiobook","title":"Other title"}]
Run Code Online (Sandbox Code Playgroud)

伟大的!正是你指定的!

解码

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

instance FromJSON Media
    where parseJSON = genericParseJSON mediaJSONOptions
Run Code Online (Sandbox Code Playgroud)

例子:

*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>
Run Code Online (Sandbox Code Playgroud)

完整的源文件

通用aeson,默认

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

正在做

import Generics.Generic.Aeson -- from generic-aeson package
Run Code Online (Sandbox Code Playgroud)

并定义:

instance ToJSON Media
    where toJSON = gtoJson
Run Code Online (Sandbox Code Playgroud)

给出结果:

[{"video":{"title":"Some title"}},{"audioBook":{"title":"Other title"}}]
Run Code Online (Sandbox Code Playgroud)

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

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

完整的源文件。)

aeson, ObjectWithSingleField

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

让我们试试这个:

mediaJSONOptions = 
    defaultOptions{ sumEncoding = ObjectWithSingleField
                  , constructorTagModifier = fmap Char.toLower
                  }
Run Code Online (Sandbox Code Playgroud)

是的,结果是:

[{"video":{"title":"Some title"}},{"audiobook":{"title":"Other title"}}]
Run Code Online (Sandbox Code Playgroud)

其余选项: (aeson, TwoElemArray)

一个可用的选项sumEncoding已经从上面的考虑排除在外,因为它提供了一个数组,它是不是很类似JSON表示询问。它是TwoElemArray。例子:

[["video",{"title":"Some title"}],["audiobook",{"title":"Other title"}]]
Run Code Online (Sandbox Code Playgroud)

是(谁)给的:

mediaJSONOptions = 
    defaultOptions{ sumEncoding = TwoElemArray
                  , constructorTagModifier = fmap Char.toLower
                  }
Run Code Online (Sandbox Code Playgroud)