Haskell:: Aeson::基于字段值解析ADT

时间:2022-12-21 17:00:48

I'm using an external API which returns JSON responses. One of the responses is an array of objects and these objects are identified by the field value inside them. I'm having some trouble understanding how the parsing of such JSON response could be done with Aeson.

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

Here is a simplified version of my problem:

下面是我的问题的简化版本:

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

instance FromJSON Content

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

In API documentation it is said that the object can be identified by the field objectClass which has value "video" for our Video object and "audiobook" for our AudioBook and so on. Example JSON:

在API文档中,我们说对象可以由字段objectClass标识,字段objectClass为我们的视频对象提供值“video”,为我们的audiobook等提供值“audiobook”。示例JSON:

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

The question is how can this type of JSON be approached using Aeson?

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

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

2 个解决方案

#1


7  

You basically need a function Text -> Text -> Media:

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

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

The FromJSON instance is now really simple (using <$> and <*> from Control.Applicative):

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

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

However, at this point you're redundant: the objectClass field in Video or Audio doesn't give you more information than the actual type, so you might remove it:

但是,此时您是多余的:视频或音频中的objectClass字段不会提供比实际类型更多的信息,因此您可以删除它:

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

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

Also note that toMedia is partial. You probably want to catch invalid "objectClass" values:

还要注意,《托马斯》是片面的。您可能希望捕获无效的“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"
-}

And last, but not least, remember that valid JSON uses strings for the name.

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

#2


2  

The default translation for a data type like:

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

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

is actually very close to what you want. (For the simplicity of my examples, I define ToJSON instances and encode the examples to see what kind of JSON we get.)

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

aeson, default

So, with the default instance we have (view the complete source file which produces this output):

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

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

Let's see whether we can get even closer with custom options...

让我们看看我们是否能更接近自定义选项……

aeson, custom tagFieldName

With custom options:

使用自定义选项:

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

instance ToJSON Media
    where toJSON = genericToJSON mediaJSONOptions

we get:

我们得到:

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

(Think yourself what you want to do with an undefined field in the real code.)

(想想你自己想要在真正的代码中处理一个未定义的字段。)

aeson, custom constructorTagModifier

Adding

添加

              , constructorTagModifier = fmap Char.toLower

to mediaJSONOptions gives:

mediaJSONOptions给:

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

Great! Exactly what you specified!

太棒了!你指定的!

decoding

Simply add an instance with the same options to be able to decode from this format:

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

instance FromJSON Media
    where parseJSON = genericParseJSON mediaJSONOptions

Example:

例子:

*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, default

To get a more complete picture, let's also look at what generic-aeson package would give (at hackage). It has also nice default translations, different in some respects from those from aeson.

为了得到更完整的图像,我们还来看看通用-aeson包(在hackage)会提供什么。它也有很好的默认翻译,在某些方面不同于伊索。

Doing

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

and defining:

和定义:

instance ToJSON Media
    where toJSON = gtoJson

gives the result:

给出了结果:

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

So, it's different from all what we've seen when using aeson.

这和我们在使用aeson时看到的所有东西都不一样。

generic-aeson's options (Settings) are not interesting for us (they allow only to strip a prefix).

泛型-aeson的选项(设置)对我们来说并不有趣(它们只允许带一个前缀)。

(The complete source file.)

(完整的源文件。)

aeson, ObjectWithSingleField

Apart from lower-casing the first letter of the constructor names, generic-aeson's translation seems similar to an option available in aeson:

除了把构造函数名的第一个字母用小写字母表示,通用-伊索的翻译似乎类似于伊索的一个选项:

Let's try this:

让我们试试这个:

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

and yes, the result is:

是的,结果是:

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

the rest of options: (aeson, TwoElemArray)

One available option for sumEncoding has been left out from consideration above, because it gives an array which is not quite similar to the JSON representation asked about. It's TwoElemArray. Example:

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

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

is given by:

是由:

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

#1


7  

You basically need a function Text -> Text -> Media:

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

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

The FromJSON instance is now really simple (using <$> and <*> from Control.Applicative):

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

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

However, at this point you're redundant: the objectClass field in Video or Audio doesn't give you more information than the actual type, so you might remove it:

但是,此时您是多余的:视频或音频中的objectClass字段不会提供比实际类型更多的信息,因此您可以删除它:

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

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

Also note that toMedia is partial. You probably want to catch invalid "objectClass" values:

还要注意,《托马斯》是片面的。您可能希望捕获无效的“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"
-}

And last, but not least, remember that valid JSON uses strings for the name.

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

#2


2  

The default translation for a data type like:

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

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

is actually very close to what you want. (For the simplicity of my examples, I define ToJSON instances and encode the examples to see what kind of JSON we get.)

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

aeson, default

So, with the default instance we have (view the complete source file which produces this output):

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

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

Let's see whether we can get even closer with custom options...

让我们看看我们是否能更接近自定义选项……

aeson, custom tagFieldName

With custom options:

使用自定义选项:

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

instance ToJSON Media
    where toJSON = genericToJSON mediaJSONOptions

we get:

我们得到:

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

(Think yourself what you want to do with an undefined field in the real code.)

(想想你自己想要在真正的代码中处理一个未定义的字段。)

aeson, custom constructorTagModifier

Adding

添加

              , constructorTagModifier = fmap Char.toLower

to mediaJSONOptions gives:

mediaJSONOptions给:

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

Great! Exactly what you specified!

太棒了!你指定的!

decoding

Simply add an instance with the same options to be able to decode from this format:

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

instance FromJSON Media
    where parseJSON = genericParseJSON mediaJSONOptions

Example:

例子:

*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, default

To get a more complete picture, let's also look at what generic-aeson package would give (at hackage). It has also nice default translations, different in some respects from those from aeson.

为了得到更完整的图像,我们还来看看通用-aeson包(在hackage)会提供什么。它也有很好的默认翻译,在某些方面不同于伊索。

Doing

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

and defining:

和定义:

instance ToJSON Media
    where toJSON = gtoJson

gives the result:

给出了结果:

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

So, it's different from all what we've seen when using aeson.

这和我们在使用aeson时看到的所有东西都不一样。

generic-aeson's options (Settings) are not interesting for us (they allow only to strip a prefix).

泛型-aeson的选项(设置)对我们来说并不有趣(它们只允许带一个前缀)。

(The complete source file.)

(完整的源文件。)

aeson, ObjectWithSingleField

Apart from lower-casing the first letter of the constructor names, generic-aeson's translation seems similar to an option available in aeson:

除了把构造函数名的第一个字母用小写字母表示,通用-伊索的翻译似乎类似于伊索的一个选项:

Let's try this:

让我们试试这个:

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

and yes, the result is:

是的,结果是:

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

the rest of options: (aeson, TwoElemArray)

One available option for sumEncoding has been left out from consideration above, because it gives an array which is not quite similar to the JSON representation asked about. It's TwoElemArray. Example:

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

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

is given by:

是由:

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