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>
完整的源文件。
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的选项(设置)对我们来说并不有趣(它们只允许带一个前缀)。
(完整的源文件。)
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>
完整的源文件。
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的选项(设置)对我们来说并不有趣(它们只允许带一个前缀)。
(完整的源文件。)
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
}