Haskell:: Aeson:: parse ADT на основе значения поля
Я использую внешний API, который возвращает ответы JSON. Одним из ответов является массив объектов, и эти объекты идентифицируются значением поля внутри них. У меня возникли проблемы с пониманием того, как анализ ASON-ответа может быть выполнен с помощью Aeson.
Вот упрощенная версия моей проблемы:
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"}]
Вопрос в том, как можно использовать этот тип JSON с помощью Aeson?
instance FromJSON Media where
parseJSON (Object x) = ???
2 ответа
Вам в основном нужна функция 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"
Тем не менее, на данный момент вы избыточны: objectClass
поле в Video
или же Audio
не дает вам больше информации, чем фактический тип, поэтому вы можете удалить его:
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
С настраиваемыми параметрами:
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
Добавление
, 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
,
дела
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"}}]
остальные варианты: (aeson, TwoElemArray
)
Один доступный вариант для sumEncoding
был исключен из рассмотрения выше, потому что он дает массив, который не совсем похож на представление JSON, о котором спрашивают. Это TwoElemArray
, Пример:
[["video",{"title":"Some title"}],["audiobook",{"title":"Other title"}]]
дан кем-то:
mediaJSONOptions =
defaultOptions{ sumEncoding = TwoElemArray
, constructorTagModifier = fmap Char.toLower
}