[英]Haskell :: Aeson :: parse ADT based on field value
我正在使用返回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具有 “有聲讀物”等等。 示例JSON:
[{objectClass: "video", title: "Some title"}
,{objectClass: "audiobook", title: "Other title"}]
問題是如何使用Aeson來接近這種類型的JSON?
instance FromJSON Media where
parseJSON (Object x) = ???
你基本上需要一個函數Text -> Text -> Media
:
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"
但是,此時您是多余的: Video
或Audio
中的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使用字符串作為名稱。
數據類型的默認轉換,如:
data Media = Video { title :: Text }
| AudioBook { title :: Text }
deriving Generic
實際上非常接近你想要的。 (為了簡化我的示例,我定義了ToJSON
實例並對示例進行編碼以查看我們獲得的JSON類型。)
因此,使用我們的默認實例(查看生成此輸出的完整源文件 ):
[{"tag":"Video","title":"Some title"},{"tag":"AudioBook","title":"Other title"}]
讓我們看看我們是否可以更接近自定義選項......
tagFieldName
,自定義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"}]
(想一想你想用真實代碼中的未定義字段做什么。)
constructorTagModifier
TagModifier 添加
, 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
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的選項( 設置 )對我們來說並不感興趣(它們只允許去除前綴)。
( 完整的源文件 。)
除了下限構造函數名稱的第一個字母外, generic-aeson
的翻譯看起來類似於aeson
提供的選項:
我們試試這個:
mediaJSONOptions =
defaultOptions{ sumEncoding = ObjectWithSingleField
, constructorTagModifier = fmap Char.toLower
}
是的,結果是:
[{"video":{"title":"Some title"}},{"audiobook":{"title":"Other title"}}]
TwoElemArray
, TwoElemArray
) 上面考慮了一個可用的sumEncoding
選項 ,因為它給出了一個與所詢問的JSON表示不太相似的數組。 這是TwoElemArray
。 例:
[["video",{"title":"Some title"}],["audiobook",{"title":"Other title"}]]
是(誰)給的:
mediaJSONOptions =
defaultOptions{ sumEncoding = TwoElemArray
, constructorTagModifier = fmap Char.toLower
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.