Использование дженериков Aeson для создания JSON со значением в качестве ключа, содержащего другое значение

Немного поиграл с gist API gist, пытаясь разобраться с библиотекой Aeson JSON. У меня возникла проблема со сгенерированным экземпляром ToJSON, и я точно не знаю, как ее решить.

Мне нужно содержать значение внутри, и ключ, связанный со значением, также должен быть значением, а не предопределенным именем ключа. Немного проще показать. Желаемый результат,

{
    "public": true, 
    "description": "Something..", 
    "files": {"This Thing.md": {"content": "Here we go!"}}
}

где значение имени файла содержит содержимое, но в настоящее время я получаю

{
    "public": true, 
    "description": "Something..", 
    "files": {"filename": "This Thing.md", "content": "Here we go!"}
}

Что мне совсем не нужно. Текущий код,

{-# LANGUAGE OverloadedStrings, DeriveGeneric #-}
import Data.Text (Text)
import Data.Aeson
import GHC.Generics

data GistContent = GistContent
    { filename :: Text
    , content :: Text
      } deriving (Show, Generic)

instance ToJSON GistContent

data Gist = Gist
    { description :: Text
    , public      :: Bool
    , files       :: GistContent
      } deriving (Show, Generic)

instance ToJSON Gist

Если предположить, что это возможно, как моя структура данных должна выглядеть, чтобы получить желаемый результат?.. И если это невозможно с использованием дженериков, как я добился этого с помощью экземпляра ToJSON (я не совсем понимаю вне структуры там тоже)?


person Tehnix    schedule 26.01.2014    source источник


Ответы (2)


Ваша проблема связана с неправильной схемой. files в настоящее время может содержать только один GistContent, что является ненужным ограничением. Вместо этого вы хотели бы иметь список GistContents:

data Gist = Gist
    { description :: Text
    , public      :: Bool
    , files       :: [GistContent]
    } deriving (Show, Generic)

Теперь рассмотрим другое ограничение на Gist: у каждого GistContent должен быть свой filename. Структура данных, которая обеспечит это, будет Data.HashMap.Strict.HashMap. Извлечение filename из GistContent и использование имени файла в качестве ключа:

data GistContent = GistContent
    { content :: Text
    } deriving (Show, Generic)

data Gist = Gist
    { description :: Text
    , public      :: Bool
    , files       :: HashMap Text GistContent
    } deriving (Show, Generic)

Все работает.

person icktoofay    schedule 26.01.2014
comment
Круто, это сработало :) Не знаю, почему HashMaps никогда не приходил мне в голову, Эсон все еще немного незнакомец, но я надеюсь, что скоро преодолею этот барьер. - person Tehnix; 26.01.2014
comment
Может быть полезно посмотреть исходный код экземпляра HashMap (github.com/bos/aeson/blob/master/Data/Aeson/Types/) — HashMap фактически используется в качестве внутренней структуры в объектах Aeson (github.com/bos/aeson/blob/master/Data/Aeson/Types/), что делает его особенно кратким. - person GS - Apologise to Monica; 26.01.2014
comment
@icktoofay - вы имели в виду Data.HashMap.*Strict*.HashMap выше? - person GS - Apologise to Monica; 26.01.2014
comment
@Ganesh: Я сделал, хотя оба, кажется, работают. Хорошо поймал. - person icktoofay; 27.01.2014

Вот экземпляр, написанный вручную (см. документацию для класса):

instance ToJSON GistContent where
   toJSON (GistContent { filename = f, content = c }) = object [f .= c]

Я сомневаюсь, что будет какой-либо способ получить это с вашим существующим типом данных с автоматически сгенерированными экземплярами, потому что все, что они могут сделать, это следовать типу данных, используя стандартную схему. Обратите внимание, что вы по-прежнему можете использовать общий экземпляр для Gist, потому что это вызовет (не универсальный) экземпляр для GistContent.

person GS - Apologise to Monica    schedule 26.01.2014