簡體   English   中英

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

[英]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"

但是,此時您是多余的: VideoAudio中的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"}]

(想一想你想用真實代碼中的未定義字段做什么。)

aeson,自定義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,默認

為了獲得更完整的圖片,讓我們看看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的選項( 設置 )對我們來說並不感興趣(它們只允許去除前綴)。

完整的源文件 。)

aeson,ObjectWithSingleField

除了下限構造函數名稱的第一個字母外, generic-aeson的翻譯看起來類似於aeson提供的選項:

我們試試這個:

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

是的,結果是:

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

其余選項:( TwoElemArrayTwoElemArray

上面考慮了一個可用的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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM