Aeson : generics with default values - haskell

Today I wanted to solve next problem.
Assume that we have typeclass DataWithDefault defined as
class DataWithDefault a where
defaultValue :: a
And we have data Example defined as
data Example =
Example { field1 :: Text
, field2 :: Text
} deriving (Show)
instance DataWithDefault Example where
defaultValue = Example "Hello" "World"
instance FromJSON Example where
parseJSON (Object v) =
Example <$> v .:? "field1" .!= field1 defaultValue
<*> v .:? "field2" .!= field2 defaultValue
parseJSON _ = mzero
instance ToJSON Example where
toJSON (Example f1 f2) =
object [ "field1" .= f1
, "field2" .= f2
]
I know that Aeson uses Generics to derive FromJSON and ToJSON instances automatically, but I can't figure out how to make it derive FromJSON instance with default values for fields that are not represented in given json. Is it possible to do using generics? Actually I don't ask you a final solution, but maybe some clue?
Update
Let me add more information about the problem.
Suppose now that you need to update your Example data and now it defined as
data Example =
Example { field1 :: Text
, field2 :: Text
, field3 :: Int
} deriving (Show)
So you want to update DataWithDefault instance declaration
instance DataWithDefault Example where
defaultValue = Example "Hello" "World" 12
And what I want to do is not to write
instance FromJSON Example where
parseJSON (Object v) =
Example <$> v .:? "field1" .!= field1 defaultValue
<*> v .:? "field2" .!= field2 defaultValue
<*> v .:? "field3" .!= field3 defaultValue
parseJSON _ = mzero
And want to derive such instance definition automatically. And more importantly, I want to do it not only for Example, but for DataWithDefault a.
Update 2
The point of combining .:? and .!= is to get as much as possible fields from given json and set every missing field to it's default value. So when we pass
{ "field1" : "space", "field2" : "ship" }
I want my new example be not field1 = Hello; field2 = World; field3 = 12, but field1 = space; field2 = ship; field3 = 12.

Instead of making Aeson do it, just use a newtype for what they were designed for:
newtype DefaultJSON a = DefaultJSON { unDefaultJSON :: a }
instance (FromJSON a, DataWithDefault a) => FromJSON (DefaultJSON a) where
parseJSON v = DefaultJSON <$> (parseJSON v <|> pure defaultValue)
Then you can do
> decode "{}" :: Maybe (DefaultJSON Example)
Just (DefaultJSON {unDefaultJSON = (Example {field1 = "Hello", field2 = "World"}})
This is a little different than what you asked, it provides a default value in case parsing fails, but not a default value for each field in case that individual field is missing.

Related

Lens through the maybe field

I have a data similar to, but a bit more nested and of course named differently:
data AppModel = AppModel
{ _foo :: Maybe String
} deriving (Eq, Show)
I would like to pass it to the Monomer textField
textField :: WidgetEvent e => ALens' s Text -> WidgetNode s e
textField field = textField_ field def
I found it can be lensed with _Just prism, but when appling it in the way it was suggested https://stackoverflow.com/a/44624359/11684473
textField $ foo . _Just . packed
It fails with:
No instance for (Applicative (Control.Lens.Internal.Context.Pretext (->) Text Text)) arising from a use of ‘_Just’
**What will be the proper way to expose the maybe value in the textField?

Serialization of a basic sum type in Json with Aeson

type GoalDescription = Text
data GoalStatus = Created | Accomplished | InProgress | GivenUp deriving (Show , Eq , Generic )
data Goal = Goal {workspaceId ::WorkspaceId , goalId :: GoalId , description :: GoalDescription , status :: GoalStatus} deriving (Show , Eq , Generic )
instance ToJSON Goal where
toJSON (Goal {workspaceId, goalId ,description,status } ) = object [
"workspaceId" .= workspaceId,
"goalId" .= goalId,
"description" .= description,
"status" .= status]
instance FromJSON Goal where
parseJSON (Object jsonObject) = Goal <$> jsonObject .: "workspaceId" <*> jsonObject .: "goalId" <*> jsonObject .: "description" <*> jsonObject .: "status"
parseJSON _ = error $ "Json format not expected"
I want to implement the FromJSON and ToJSON of GoalStatus that way: Goal {.. status:"accomplished"} or Goal {.. status:"inProgress"} etc... somehow I don't know how to implement these type classes without having a key -> value structure... GoalStatus should be only converted into a String Text without Keys attached to the value..
I have this temporary solution where I had to add an unnecessary key named "value" :
instance ToJSON GoalStatus where
toJSON (Created) = object ["value" .= String "created"]
toJSON (InProgress) = object ["value" .= String "inProgress"]
toJSON (Accomplished) = object ["value" .= String "accomplished"]
toJSON (GivenUp) = object ["value" .= String "GivenUp"]
instance FromJSON GoalStatus where
parseJSON (Object o) = do
value <- o .: "value"
case value of
String status | (unpack status) == "created" -> return Created
String status | (unpack status) == "inProgress" -> return InProgress
String status | (unpack status) == "accomplished" -> return Accomplished
String status | (unpack status) == "accomplished" -> return GivenUp
_ -> error $ "Json format not expected"
parseJSON _ = error $ "Json format not expected"
String !Text is a constructor of Value and object has the type signature [Pair] -> Value where Pair is (Text, Value). You can use String to make the Value in ToJSON and then match on the particular shapes of the String when parsing in FromJSON.
instance ToJSON GoalStatus where
toJSON (Created) = String "created"
toJSON (InProgress) = String "inProgress"
toJSON (Accomplished) = String "accomplished"
toJSON (GivenUp) = String "givenUp"
instance FromJSON GoalStatus where
parseJSON (String s) = case unpack s of
"created" -> return Created
"inProgress" -> return InProgress
"accomplished" -> return Accomplished
"givenUp" -> return GivenUp
_ -> error $ "Json format not expected"
parseJSON _ = error $ "Json format not expected"
I'm not sure that I understand the question. Here's a full file with generic-derived implementations:
{-# LANGUAGE DeriveGeneric #-}
module Q54178405 where
import Data.Text
import Data.Aeson
import GHC.Generics
type WorkspaceId = Int
type GoalId = Int
type GoalDescription = Text
data GoalStatus =
Created | Accomplished | InProgress | GivenUp deriving (Show, Eq, Generic)
instance ToJSON GoalStatus
instance FromJSON GoalStatus
data Goal = Goal {
workspaceId ::WorkspaceId
, goalId :: GoalId
, description :: GoalDescription
, status :: GoalStatus}
deriving (Show, Eq, Generic)
instance ToJSON Goal
instance FromJSON Goal
Here's how it behaves in GHCi:
*Q54178405 Q54178405> encode $ Goal 42 1337 "foo" Accomplished
"{\"status\":\"Accomplished\",\"goalId\":1337,\"workspaceId\":42,\"description\":\"foo\"}"
*Q54178405 Q54178405> encode $ Goal 42 1337 "foo" GivenUp
"{\"status\":\"GivenUp\",\"goalId\":1337,\"workspaceId\":42,\"description\":\"foo\"}"
Isn't that what you want?
The instances round-trip as well:
*Q54178405 Q54178405> decode $ encode $ Goal 42 1337 "foo" GivenUp :: Maybe Goal
Just (Goal {workspaceId = 42, goalId = 1337, description = "foo", status = GivenUp})
If this isn't what you want, it'd be useful with some explicit examples of input with desired output.

Aeson with arrays of arrays, of which some are empty

I have the unfortunate data to work with:
{ "name": "foo"
, "data": [ []
, ["a", "b", "c", 1]
, ["d", "e", "f", 2] ] }
The data entries are allowed to be either empty array, or an array of size four.
That I want to parse into:
data ResultRow = ResultRow Text Text Text Int deriving (Show, Generic)
data ResultSet =
ResultSet { f_name :: Text
, f_data :: [Maybe ResultRow] } deriving (Show, Generic)
The following however does not accept the empty arrays:
customOptions = defaultOptions { fieldLabelModifier = drop 2 }
instance FromJSON ResultRow where
parseJSON = genericParseJSON customOptions
instance FromJSON ResultSet where
parseJSON = genericParseJSON customOptions
The error message is:
Left "Error in $[1].data[0]: When expecting a product of 4 values, encountered an Array of 0 elements instead"
I've also tried putting an extra type around [Maybe ResultRow] and have that convert the sub-arrays to lists and pattern match on [], and dispatch the non-empty case to the ResultRow parser but I simply couldn't get it to compile and got lost in the error messages.
Ideally I would like to have some way of skipping the empty arrays as they're just noise in the data. I have no control of the producer of the data.
Like dsvensson, I'm puzzled that this doesn't 'just work' out of the box, since both [a] and Maybe a are FromJSON instances when a is. Since I already ended up spending way too much time on this, I can't offer an explanation, but I can offer a workaround. Hopefully someone more knowledgeable can give a better answer.
Instead of defining f_data as a [Maybe ResultRow], you can define a newtype that wraps Maybe ResultRow:
newtype MaybeResultRow = MaybeResultRow (Maybe ResultRow) deriving (Show, Generic)
You can give this type special FromJSON behaviour:
instance FromJSON MaybeResultRow where
parseJSON v =
case fromJSON v of
Success rr -> return $ MaybeResultRow $ Just rr
_ -> return $ MaybeResultRow Nothing
This, obviously, implies a change of ResultSet:
data ResultSet =
ResultSet { f_name :: Text
, f_data :: [MaybeResultRow] } deriving (Show, Generic)
In order to test, I defined this JSON document:
myJson :: ByteString
myJson =
"{\
\\"name\": \"foo\",\
\\"data\": [\
\[],\
\[\"a\", \"b\", \"c\", 1],\
\[\"d\", \"e\", \"f\", 2]\
\]\
\}"
Loading it all into GHCi, it looks like it's working:
*Lib Lib> decode myJson :: Maybe ResultSet
Just (ResultSet {
f_name = "foo"
, f_data = [
MaybeResultRow Nothing,
MaybeResultRow (Just (ResultRow "a" "b" "c" 1)),
MaybeResultRow (Just (ResultRow "d" "e" "f" 2))]})
Here, I've taken the liberty to format the output from GHCi in order to enhance readability.
I trust that you can figure out how to unwrap and filter the list of MaybeResultRow values...
I stole the solution of using fromJSON and matching on Success from this answer.

Aeson decode JSON object that can be either a string or an int

I'm working with some complexly formatted JSON responses from a REST server. To decode them, I have a couple of data types to handle the different nested objects. For example:
... Other types ...
data Profile =
Profile { fields :: [KVPair]
} deriving (Show)
instance FromJSON Profile where
parseJSON (Object v) =
Profile <$> v .: "Fields"
parseJSON _ = mzero
data KVPair =
KVPair { key :: Int
, value :: String
} deriving (Show)
instance FromJSON KVPair where
parseJSON (Object v) =
KVPair <$> v .: "Key"
<*> v .: "Value"
parseJSON _ = mzero
Everything works except for the final KVPair type. My JSON objects all have integer keys; however, the values can be either an integer or a string:
{
"Key": 0,
"Value": "String Value!"
},
{
"Key": 1,
"Value": 42
}
Now I suppose I could add another sum type to my value decode that is composed of String and Int, but I would prefer to avoid adding a whole new type just for that. Does Aeson have a simple way to handle this scenario?
There are two simple fixes. One is to simply write
data KVPair = KVPair { key :: Int, value :: Value }
and leave all other code the same. Consumers will need to check the Value to see whether it is a string-y thing or a number-y thing.
Probably the better way is to simply provide two alternative parsers that both convert to your desired format. For example, keeping your KVPair definition as is, one might write
showInt :: Int -> String
showInt = show
instance FromJSON KVPair where
parseJSON (Object v)
= KVPair
<$> v .: "Key"
<*> (v .: "Value" <|> (showInt <$> v .: "Value"))
The best of both worlds would be to keep the information about whether it's a String or Int around and to reject other kinds of values; e.g.
data KVPair = KVPair { key :: Int, value :: Either String Int }
instance FromJSON KVPair where
parseJSON (Object v)
= KVPair
<$> v .: "Key"
<*> ( (Left <$> v .: "Value")
<|> (Right <$> v .: "Value")
)
You'll just have to use the Aeson Value type to work with an Object with fields that can be any JSON value.

Aeson: parsing dynamic keys as type field

Let's say there is a JSON like:
{
"bob_id" : {
"name": "bob",
"age" : 20
},
"jack_id" : {
"name": "jack",
"age" : 25
}
}
Is it possible to parse it to [Person] with Person defined like below?
data Person = Person {
id :: Text
,name :: Text
,age :: Int
}
You cannot define an instance for [Person] literally, because aeson already includes an instance for [a], however you can create a newtype, and provide an instance for that.
Aeson also includes the instance FromJSON a => FromJSON (Map Text a), which means if aeson knows how to parse something, it knows how to parse a dict of that something.
You can define a temporary datatype resembling a value in the dict, then use the Map instance to define FromJSON PersonList, where newtype PersonList = PersonList [Person]:
data PersonInfo = PersonInfo { infoName :: Text, infoAge :: Int }
instance FromJSON PersonInfo where
parseJSON (Object v) = PersonInfo <$> v .: "name" <*> v .: "age"
parseJSON _ = mzero
data Person = Person { id :: Text, name :: Text, age :: Int }
newtype PersonList = PersonList [Person]
instance FromJSON PersonList where
parseJSON v = fmap (PersonList . map (\(id, PersonInfo name age) -> Person id name age) . M.toList) $ parseJSON v
If you enable FlexibleInstances, you can make instance for [Person]. You can parse your object to Map Text Value and then parse each element in map:
{-# LANGUAGE UnicodeSyntax, OverloadedStrings, FlexibleInstances #-}
module Person (
) where
import Data.Aeson
import Data.Aeson.Types
import Data.Text.Lazy
import Data.Text.Lazy.Encoding
import Data.Map (Map)
import qualified Data.Map as M
data Person = Person {
id ∷ Text,
name ∷ Text,
age ∷ Int }
deriving (Eq, Ord, Read, Show)
instance FromJSON [Person] where
parseJSON v = do
objs ← parseJSON v ∷ Parser (Map Text Value)
sequence [withObject "person"
(\v' → Person i <$> v' .: "name" <*> v' .: "age") obj |
(i, obj) ← M.toList objs]
test ∷ Text
test = "{\"bob_id\":{\"name\":\"bob\",\"age\":20},\"jack_id\":{\"name\":\"jack\",\"age\":25}}"
res ∷ Maybe [Person]
res = decode (encodeUtf8 test)
mniip's answer converts the JSON Object to a Map, which leads to a result list sorted by ID. If you don't need the results sorted in that fashion, it's probably better to use a more direct approach to speed things up. In particular, an Object is really just a HashMap Text Value, so we can use HashMap operations to work with it.
Note that I renamed the id field to ident, because most Haskell programmers will assume that id refers to the identity function in Prelude or to the more general identity arrow in Control.Category.
module Aes where
import Control.Applicative
import Data.Aeson
import Data.Text (Text)
import qualified Data.HashMap.Strict as HMS
data PersonInfo = PersonInfo { infoName :: Text, infoAge :: Int }
instance FromJSON PersonInfo where
-- Use mniip's definition here
data Person = Person { ident :: Text, name :: Text, age :: Int }
newtype PersonList = PersonList [Person]
instance FromJSON PersonList where
parseJSON (Object v) = PersonList <$> HMS.foldrWithKey go (pure []) v
where
go i x r = (\(PersonInfo nm ag) rest -> Person i nm ag : rest) <$>
parseJSON x <*> r
parseJSON _ = empty
Note that, like Alexander VoidEx Ruchkin's answer, this sequences the conversion from PersonInfo to Person explicitly within the Parser monad. It would therefore be easy to modify it to produce a parse error if the Person fails some sort of high-level validation. Alexander's answer also demonstrates the utility of the withObject combinator, which I'd have used if I'd known it existed.

Resources