Aeson: FromJSON with nested encoded json

552 views Asked by At

I have JSON that contains an encoded JSON as string in one of its properties:

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

Given that I have a data type:

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

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

How do I implement FromJSON for Person?

2

There are 2 answers

1
Sibi On

One way of doing this is by writing your own parser with the function withObject. In that function you explicitly handle your address case. Example:

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

These are the necessary import I had to do:

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

Note that in your code, you may want to avoid the partial function fromJust. A sample way of decoding your JSON string when it is stored in a file named add.json:

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

Output on execution:

sibi::jane { ~/scripts }-> ./aeson.hs
Right (Person {firstName = "Frederick", lastName = "Krueger", address = Address {street = "Elm Street, 13", city = "Springwood", state = "OH"}})
0
ppb On

Here is my attempt using a small helper parser:

{-# 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"

and running it in a repl:

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

In brief the Person instance uses =<< operator to chain two parsers together - the one produced by o .: "address" and parseAddress. Within parseAddress we can then examine the value and do further processing if we see a String. decodeStrict is used to attempt decoding the string as an object and once parseAddress has that parseJSON can be used to parse that object as an Address.

Or as a one liner with no intermediate values:

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