What is the best way
Data.Aeson FromJSON instance
Convert Array to Object:
import Data.Aeson
data MixArray = MixArray {
vStr :: String,
vNum :: Int,
vBool :: Bool
} deriving Show
main = do
jsonStr = ["a",1,true]
mix <- eitherDecode $ jsonStr :: IO (Either String [MixArray])
show mix
to:
MixArray { vStr = "a", vNum = 1, vBool= true}
You can reuse the FromJSON instance for lists and the Value type, and then construct your type after pattern matching. If the pattern match fails, the parser will fail.
import Data.Text (unpack)
instance FromJSON MixArray where
parseJSON jsn = do
[String s, Number n, Bool b] <- parseJSON jsn
return MixArray { vStr = unpack s, vNum = truncate n, vBool = b }
Related
When trying to parse some simple JSON using Aeson I get a type error I don't understand. I have the following JSON
jsonString = "[\"a\", [\"b\", \"c\"]]" :: L.ByteString
and I have defined the following imports and code:
import Data.Aeson
import GHC.Generics
import qualified Data.ByteString.Lazy as L
data Ch = Ch {
c1 :: String,
c2 :: (String, String)
} deriving (Show, Generic)
instance FromJSON Ch
When I try to use eitherDecode on this string with my Ch type I get an error
*Aeson> eitherDecode jsonString :: Either String Ch
Left "Error in $: expected record (:*:), encountered Array"
Can someone explain me the error and tell me how I should parse this JSON?
An approach that would work is
eitherDecode jsonString :: Either String (String, (String, String))
but I'd rather go to my type directly.
If you already know of a type that parses as intended then perhaps the easiest solution is to just write your instance in terms of that type and translating:
import Data.Aeson
import GHC.Generics
import qualified Data.ByteString.Lazy as L
data Ch = Ch {
c1 :: String,
c2 :: (String, String)
} deriving (Show, Generic)
instance FromJSON Ch where
parseJSON x =
do (a,(b,c)) <- parseJSON x
pure (Ch a (b,c))
And the result is:
*Main> :set -XOverloadedStrings
*Main> eitherDecode "[\"a\", [\"b\", \"c\"]]" :: Either String Ch
Right (Ch {c1 = "a", c2 = ("b","c")})
EDIT:
A more direct use of Aeson's API can be informative or preferred:
instance FromJSON Ch where
parseJSON =
withArray "Ch" $ \arr ->
-- from Data.Aeson.Types
if V.length arr /= 2
-- ^ from Data.Vector
then typeMismatch "Length should be 2" (Array arr)
-- ^ from Data.Aeson.Types
else Ch <$> parseJSON (arr ! 0) <*> parseJSON ( arr ! 1 )
{-# LANGUAGE TemplateHaskell, DeriveGeneric, DeriveAnyClass #-}
module Main where
import Flow
import Control.Lens hiding ((|>))
import Data.Default
import GHC.Generics
main :: IO ()
main = putStrLn "hello world"
append x = (++ [x])
equals = (==)
data SectionedItems s i = SectionedItems{
_section :: Maybe s,
_items :: [i],
_subsections :: [SectionedItems s i]
} deriving (Show, Generic)
instance Default (SectionedItems s i) where
def = SectionedItems { _section = Nothing, _items = [], _subsections = [] }
makeLenses ''SectionedItems
sectionedItems = SectionedItems{
_section = Nothing,
_items = [],
_subsections = []
}
data SectionedItemsElement s i = Section s | Item i
addElementToSectionedItems :: SectionedItems s i -> SectionedItemsElement s i -> SectionedItems s i
addElementToSectionedItems si (Section x) =
(def & section .~ Just x :: SectionedItems s i) -- Error is probably somewhere here
|> \subsec -> si & subsections %~ append subsec
What do I replace and with in order to make it work? I tried s and i but I get an error Could not match actual type s1 with expected type s on Just x. What can I use to reference types s and I from the function body?
Simple fixes:
Add a type signature to equals - otherwise monomorphism bites you
Remove the unnecessary type annotation on def & section .~ Just x - the type is inferred anyways.
So:
...
equals :: Eq a => a -> a -> Bool
equals = (==)
...
addElementToSectionedItems :: SectionedItems s i -> SectionedItemsElement s i -> SectionedItems s i
addElementToSectionedItems si (Section x) =
(def & section .~ Just x)
|> \subsec -> si & subsections %~ append subsec
I need to parse an object that has a string element where the string itself is a stringified object:
{
"a" : "apples",
"bar" : "{\"b\":\"bananas\"}"
}
I would like to parse this into Just ( Foo { fooA = "apples", fooBar = Bar { barB = "bananas" } } ) so if parsing of bar returns Nothing then the parsing of the whole object returns Nothing, ie the result is as though the bar element of the object was not stringified.
Here is my attempt in which the parsing of testData returns Nothing:
{-# LANGUAGE OverloadedStrings #-}
module Main where
import Data.Aeson
import Data.Aeson.Types
data Foo = Foo { fooA :: String
, fooBar :: Bar
} deriving (Show)
instance FromJSON Foo where
parseJSON (Object o) = do bar <- (o .: "bar")
Foo <$> o .: "a" <*> parseJSON bar
parseJSON x = typeMismatch "Foo" x
data Bar = Bar { barB :: String
} deriving (Show)
instance FromJSON Bar where
parseJSON (Object o) = Bar <$> o .: "b"
parseJSON x = typeMismatch "Bar" x
testData = "{ \"a\":\"apples\", \"bar\":\"{\\\"b\\\":\\\"bananas\\\"}\" }"
main :: IO ()
main = putStrLn $ show d
where d :: Maybe Foo
d = decode testData
How can I modify the above code to perform closer to what I need?
You can get more insight into what's going on by using:
main = print (eitherDecode testData :: Either String Foo)
which displays: Left "Error in $: expected Bar, encountered String"
In this code:
parseJSON (Object o) = do bar <- (o .: "bar")
Foo <$> o .: "a" <*> parseJSON bar
bar is String ... value.
To accomplish what you want to do, you could add a case to the FromJSON instance for Bar to catch this:
instance FromJSON Bar where
...
parseJSON (String text) =
case eitherDecode (textToLBS text) of
Left e -> fail $ "while decoding a Bar: " ++ e
Right b -> return b
...
Or you could put this code in the parseJSON definition for Foo.
Here textToLBS converts strict Text to lazy ByteStrings:
import qualified Data.Text as T
import qualified Data.ByteString.Lazy as LBS
import qualified Data.Text.Encoding as TE
textToLBS :: T.Text -> LBS.ByteString
textToLBS t = LBS.fromStrict (TE.encodeUtf8 t)
Code available at: http://lpaste.net/143183
I have a data type where one of the fields is a list of one of n other data types (n is small and the types are known in advance). I would like to make a JSON parser but I can't quite figure it out. I've tried creating a Pet type class and making them both instances of it, but it seemed to be a dead end. Any help would be appreciated!
As a simplified example - I have a Person data type who can have a list of pets, either dogs or cats - but not a mix of both.
Here's the example:
{-# LANGUAGE OverloadedStrings #-}
import Control.Applicative
import Data.Aeson
import Data.ByteString.Lazy as L
import Data.Aeson.Types (Parser)
import Control.Monad (mzero)
data Person = Person {
name :: String,
species :: String,
pets :: [?] -- a list of dogs OR cats
} deriving Show
instance FromJSON (Person a) where
parseJSON (Object v) = ???
data Dog = Dog {
dogField :: String
} deriving Show
instance FromJSON Dog where
parseJSON (Object v) = Dog <$>
v .: "dogField"
data Cat = Cat {
catField :: String
} deriving Show
instance FromJSON Cat where
parseJSON (Object v) = Cat <$>
v .: "catField"
A standard way of representing either one type or another is to use the Either type, e.g.:
data Person { ..., pets :: Either [Dog] [Cat] }
Also, you might be interested in use GHC Generics to auto-derive the To/FromJSON instances.
An example with a data structure that uses Either:
{-# LANGUAGE DeriveGeneric #-}
import Data.Aeson
import GHC.Generics
data Person = Person {
name :: String,
species :: String,
pets :: Either [String] [Int]
} deriving (Show,Generic)
instance ToJSON Person -- instances are auto-derived
instance FromJSON Person
doit = do
let me = Person "user5402" "Human" (Right [42])
print $ encode me
If you have more than two alternatives you can easily create your own sum type like this:
-- assume the possible pet types are: Dog, Cat, Rodent, Reptile
data Pets = Dogs [Dog] | Cats [Cat] | Rodents [Rodent] | Reptiles [Reptile]
deriving (Show, Generic)
data Person { ..., pets :: Pets }
deriving (Show, Generic)
doit = do
let me = Person "user5402" "Human" (Rodents [agerbil, amouse])
print $ encode me
where agerbil and amouse are Rodent values.
I'm modifying #user5402's answer because I don't like the "tag" and "contents" fields that Generics added. Also accepting his answer since he gave me the key insight of how to structure the sum type
instance FromJSON Pets where
parseJSON (Object o) = (parsePets o "pets")
parseJSON _ = mzero
parsePets :: Object -> T.Text -> Parser Pets
parsePets o key = case H.lookup key o of
Nothing -> fail $ "key " ++ show key ++ " not present"
Just v -> parseToCatsOrDogs (o .: "species") v
{-# INLINE parsePets #-}
parseToCatsOrDogs :: Parser String -> Value -> Parser Pets
parseToCatsOrDogs speciesParser (Array v) = speciesParser >>= \species -> case species of
"dog" -> (V.mapM (\x -> parseJSON $ x) v) >>= \ dogVector -> return $ Dogs (V.toList dogVector)
"cat" -> (V.mapM (\x -> parseJSON $ x) v) >>= \ catVector -> return $ Cats (V.toList catVector)
_ -> mzero
parseToCatsOrDogs _ _ = mzero
I'm using this code:
{-# LANGUAGE OverloadedStrings #-}
import Data.Aeson
import Data.Maybe
import Data.ByteString.Lazy
import Control.Applicative
import Debug.Trace
import Control.Monad
import qualified Data.Aeson.Types as T
main = do
res <- liftA show (liftA decodeOriginal (Data.ByteString.Lazy.readFile "./a.json"))
Prelude.putStrLn res
interpretResult :: Maybe String -> String
interpretResult Nothing = "Error."
interpretResult x = fromJust x
data TotalLine1 = TotalLine1 {
timestamp :: Integer,
value :: Integer
} deriving (Eq, Show)
data Original = Original {
totals :: [TotalLine1]
} deriving (Eq, Show)
instance FromJSON Original where
parseJSON (Object v) = traceStack "Original" (Original <$> (parseJSON =<< (v .: "visitors.total")))
parseJSON _ = mzero
instance FromJSON TotalLine1 where
parseJSON (Object v) = TotalLine1 <$>
v .: "timestamp" <*>
v .: "value"
decodeOriginal :: ByteString -> Maybe Original
decodeOriginal b = traceStack "decoding" (do
a <- decode b :: Maybe Original
return a)
to try and parse JSON like this:
{
visitors.total: [
{
timestamp: 1365548400,
value: 1
},
{
timestamp: 1365548700,
value: 2
},
{
timestamp: 1365549000,
value: 5
},
]
}
But main just returns Nothing every time. What have I done wrong? It seems that even parseJSON isn't being called for Original.
Your JSON file is not valid.
On the one hand, the names of the fields have to be quoted,
"timestamp"
etc. and on the other, you have a trailing comma in the list of TotalLine1s, which causes the decoding of the ByteString to a Value to fail. Quote the field names and remove the trailing comma, and it works.