The trick with using Aeson
effectively is to call down to parseJSON
recursively. This is done implicitly when you use the (.:)
operator, so seeing something like M.lookup
is usually a bad sign. I'll provide a simplified example: a path of (latitude, longitude) pairs, represented by a JSON array of JSON objects.
data Path = Path { points :: [Point] }
data Point = Point { lat :: Double, lon :: Double }
-- JSON format looks a bit like this
--
-- { "points": [ {"latitude": 86, "longitude": 23} ,
-- {"latitude": 0, "longitude": 16} ,
-- {"latitude": 43, "longitude": 87} ] }
instance FromJSON Path where
parseJSON = withObject "path" $ \o ->
Path <$> o .: "points"
instance FromJSON Point where
parseJSON = withObject "point" $ \o ->
Point <$> o .: "latitude"
<*> o .: "longitude"
There are two major points to take away from this snippet. Firstly, note the use of withObject
to quickly constrain that the Value
passed to parseJSON
is tagged as an Object
—it's not significantly different than using pattern matching, but it produces automatic, uniform error messages so it's worth considering.
Secondly, and more importantly, note that I only define FromJSON
instance which describe the high-level outline of each object. In particular, examine the body of FromJSON Path
Path <$> o .: "points"
All this says is that I need to look into the entry named "points"
and try to parse it as whatever type is necessary to build a Path
—in this case, a list of Point
s, [Point]
. This use depends upon recursively defined FromJSON
instances. We need to parse an array, but fortunately there already exists the FromJSON
instance
instance FromJSON a => FromJSON [a] where ...
which is interpreted as a JSON array of whatever JSON types a
can parse as. In our case a ~ Point
, so we just define that instance
instance FromJSON Point where ...
and then recursively depend upon the
instance FromJSON Double where ...
which is quite standard.
Another important trick you can use is adjoining multiple parses with (<|>)
. I'll simplify the Response
data type a bit where it either parses as a particular Object
or fails and produces a plain, dynamically typed Value
as default. First we'll write each parser independently.
data Obj = Obj { foo :: String, bar :: String }
| Dyn Value
okParse :: Value -> Parser Obj
okParse = withObject "obj" (\o -> Obj <$> o .: "foo" <*> o .: "bar")
elseParse :: Value -> Parser Obj
elseParse v = pure (Dyn v)
And now we combine them in the actual FromJSON
instance
instance FromJSON Obj where
parseJSON v = okParse v <|> elseParse v
In this case, aeson
will try to use okParse
first and, if it fails, fall back on elseParse
. Since elseParse
is simply a pure
value it will never fail and thus provides a "default" fallback.