Aeson: FromJSON с вложенным закодированным json

У меня есть JSON, который содержит закодированную JSON как строку в одном из своих свойств:

{ 
  "firstName": "Frederick",
  "lastName": "Krueger",
  "address": "{\"street\": \"Elm Street, 13\", \"city\": \"Springwood\", \"state\": \"OH\"}"
}

Учитывая, что у меня есть тип данных:

data Address = Address { street :: String, city :: String, state :: String }
               deriving (Generic, Show)

data Person = Person { firstName :: String, lastName :: String, address :: Address }
              deriving (Generic, Show)

Как реализовать FromJSON для Person?


person Alexey Raga    schedule 18.12.2016    source источник


Ответы (2)


Вот моя попытка использовать небольшой вспомогательный парсер:

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}

module MyModule where

import           Control.Monad
import           Data.Aeson
import           Data.Aeson.Types
import           Data.ByteString (ByteString)
import qualified Data.Text.Encoding as Text
import           GHC.Generics

data Address = Address
  { street :: String
  , city :: String
  , state :: String
  } deriving (Generic, Show)

instance FromJSON Address

data Person = Person
  { firstName :: String
  , lastName :: String
  , address :: Address
  } deriving (Generic, Show)

instance FromJSON Person where
  parseJSON (Object o) =
    Person <$> o .: "firstName" <*> o .: "lastName" <*>
    (parseAddress =<< o .: "address")
  parseJSON _ = mzero

parseAddress :: Value -> Parser Address
parseAddress (String s) = do
  let maybeObject = decodeStrict (Text.encodeUtf8 s)
  case maybeObject of
    Nothing -> mzero
    Just o -> parseJSON (Object o)
parseAddress _ = mzero

testString :: ByteString
testString =
  "{\n  \"firstName\": \"Frederick\",\n  \"lastName\": \"Krueger\",\n\"address\": \"{\\\"street\\\": \\\"Elm Street, 13\\\", \\\"city\\\": \\\"Springwood\\\", \\\"state\\\": \\\"OH\\\"}\"\n}\n"

и запустив его в ответе:

λ> eitherDecodeStrict testString :: Either String Person
Right (Person {firstName = "Frederick", lastName = "Krueger", address = Address {street = "Elm Street, 13", city = "Springwood", state = "OH"}})

Вкратце, экземпляр Person использует оператор =<< для объединения двух парсеров - одного, созданного o .: "address" и parseAddress. Затем в parseAddress мы можем проверить значение и продолжить обработку, если увидим String. decodeStrict используется для попытки декодирования строки как объекта, и когда parseAddress имеет это parseJSON, может использоваться для синтаксического анализа этого объекта как Address.

Или как один лайнер без промежуточных значений:

parseAddress :: Value -> Parser Address
parseAddress (String s) =
  either (const mzero) parseJSON (eitherDecodeStrict (Text.encodeUtf8 s))
parseAddress _ = mzero
person ppb    schedule 18.12.2016

Один из способов сделать это - написать собственный синтаксический анализатор с функцией withObject. В этой функции вы явно обрабатываете регистр своего адреса. Пример:

parsePerson :: Value -> Parser Person
parsePerson = withObject "expected person"
              (\obj -> do
                 fname <- obj .: "firstName"
                 lname <- obj .: "lastName"
                 (addr :: String) <- obj .: "address"
                 let addr' = decode (BS.fromStrict $ pack addr)
                 return $ Person { firstName = fname, lastName = lname, address = fromJust addr' })

instance FromJSON Address where
    parseJSON (Object v) = Address <$>
                           v .: "street" <*>
                           v .: "city" <*>
                           v .: "state"
    parseJSON _ = empty

Это необходимый импорт, который мне нужно было сделать:

import Data.Aeson
import Data.Aeson.Types (Parser(..), parseEither)
import Data.Maybe (fromJust)
import Data.Text (Text)
import Data.ByteString.Char8 (pack)
import qualified Data.ByteString.Lazy as BS

Обратите внимание, что в вашем коде вы можете избежать частичной функции fromJust. Пример способа декодирования вашей строки JSON, когда она хранится в файле с именем add.json:

main :: IO ()
main = do
  dat <- BS.readFile "./add.json"
  let val = decode dat :: Maybe Value
  print $ (parseEither parsePerson (fromJust val))

Выход при исполнении:

sibi::jane { ~/scripts }-> ./aeson.hs
Right (Person {firstName = "Frederick", lastName = "Krueger", address = Address {street = "Elm Street, 13", city = "Springwood", state = "OH"}})
person Sibi    schedule 18.12.2016
comment
Я вижу, вы здесь используете fromJust, что происходит, когда это Nothing? Исключение во время выполнения или без разбора? - person Alexey Raga; 18.12.2016