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
                  }
Другие вопросы по тегам