Search code examples
haskellmonadscompositionparsecaeson

Composing optional Aeson parsers


Here is a part of JSON object which represents the user:

{ "image": { "url": "http://example.com" } }

I need to parse it into a User type:

data User = User { imgUrl :: Maybe Text }

The naive solution:

parseJSON (Object o) = User <$> getImgUrl o
    where getImgUrl o = (o .:? "image") >>= maybe (return Nothing) (.:? "url")

But that's not much better than these chains:

case f m1 of
    Nothing -> Nothing
    Just m2 -> case f2 m2 of
        Nothing -> Nothing
        Just m3 -> case f3 m3 ....

that are often demonstrated in 'Why do you need a Monad' explanations

Thus, I need to compose parsers which look like (.:? "url") :: Parser (Maybe a)

I've tried to describe that composition with comp function:

getImgUrl :: Object -> Parser (Maybe Text)
getImgUrl o = o .:? "image" >>= comp (o .:? "url")

comp :: (Monad m) => (a -> m (Maybe b)) -> Maybe a -> m (Maybe b)
comp p Nothing = return Nothing
comp p (Just o) = p o

Smells like a Functor, but fmap didn't help me.

Then I decided, that composition must go on:

getImgUrl :: Object -> Parser (Maybe Text)
getImgUrl = comp2 (.:? "image") (.:? "url") o

-- Maybe should be changed to a matching typeclass
comp2 :: (Monad m) => (a -> m (Maybe b)) -> (b -> m (Maybe c)) -> a -> m (Maybe c)
comp2 = undefined

Hoogle search didn't help me, but skimming through Control.Monad docs gave me Kliesli composition, which I am not experienced with. I see some similarity:

(>=>) :: Monad m => (a -> m    b)  -> (b -> m c)     -> a -> m    c
comp2 :: Monad m => (a -> m (f b)) -> (b -> m (f c)) -> a -> m (f c)

The difference is that during the composition Maybe should be 'unwrapped'.

It seems that I am close to the solution, but still can't find it. Please give me some insight.

[Update]: I've decided that the best solution to the actual problem, would be to preserve the original JSON structure and to have a nested User type:

data User = User { image :: Maybe Image }
data Image = Image { url :: Text }

This completely eliminates my problem, and makes API more compatible with the original source.

However, just for theoretical purposes, it would be great to see how the original problem could be solved.


Solution

  • I was pointed to a nice solution

    To start with, here is how we can do it.

    parseJSON (Object o) = User . join <$> (traverse (.:? "url") =<< (o .:? "image"))
    

    Here, we get Parser (Maybe Object) and pass it to the next monadic action, which works with Maybe Object. With the help of traverse we perform the action if it was Just. In the result we get Parser (Maybe (Maybe Object)). What's left is tojointhat result and getParser (Maybe Object)`.

    However it would be nice to make it simpler to use. I would take this operator from @bheklilr's answer and will adopt it to this solution.

    -- The type can be much more generic, but for simplicity I would keep it in domain of the problem
    (.:?>) :: FromJSON a => Parser (Maybe Object) -> Text -> Parser (Maybe a)
    maybeParser .:?> key = fmap join . traverse (.:? key) =<< maybeParser
    

    And after that we can use that operator to parse long chains of optional fields.

    getImgUrl :: A.Object -> Parser (Maybe Text)
    getImgUrl o = o .:? "image" .:?> "url" .:?> "foo" .:?> "bar"
    

    From practical point of view, this solution is not much more useful than @bheklilr's solution and my initial 'naive' code sample. However, I like it much more because instead of matching on Just/Nothing it can transform many other types (e.g. Either)