From JSON to sum type

For a while I’ve been planning to take full ownership of the JSON serialisation and parsing in cblrepo. The recent inclusion of instances of ToJSON and FromJSON for Version pushed me to take the first step by writing my own instances for all external types.

When doing this I noticed that all examples in the aeson docs use a product

data Person = Person {
      name :: Text
    , age  :: Int
    }

whereas I had to deal with quite a few sums, e.g. VersionRange. At first I struggled a little with how to write an instance of FromJSON. After quite a bit of thinking I came up with the following, which I think is fairly nice, but I’d really like to hear what others think about it. Maybe I’ve just missed a much simpler way of implementing parseJSON:

instance FromJSON V.VersionRange where
  parseJSON = withObject "VersionRange" go
    where
      go o = do
        lv <- (o .:? "LaterVersion") >>= return . fmap V.laterVersion
        tv <- (o .:? "ThisVersion") >>= return . fmap V.thisVersion
        ev <- (o .:? "EarlierVersion") >>= return . fmap V.earlierVersion
        av <- (o .:? "AnyVersion") >>= \ (_::Maybe [(Int,Int)]) -> return $ Just V.anyVersion
        wv <- (o .:? "WildcardVersion") >>= return . fmap V.WildcardVersion
        uvr <- (o .:? "UnionVersionRanges") >>= return . fmap toUvr
        ivr <- (o .:? "IntersectVersionRanges") >>= return . fmap toIvr
        vrp <- (o .:? "VersionRangeParens") >>= return . fmap V.VersionRangeParens
        maybe (typeMismatch "VersionRange" $ Object o)
          return
          (lv <|> tv <|> ev <|> uvr <|> ivr <|> wv <|> vrp <|> av)

      toUvr [v0, v1] = V.unionVersionRanges v0 v1
      toIvr [v0, v1] = V.intersectVersionRanges v0 v1

Any and all comments and suggestions are more than welcome!

⟸ Freer play with effects Final version of JSON to sum type ⟹

David Turner

Isn’t

f >>= return . g

The same as

fmap g f

?

Or something like that anyway?

Magnus Therning

@David, yes, f >>= return . g is the same as fmap g f. However, since in this case g is of the form fmap h it would end up looking like

fmap (fmap h) f

and then I find f >>= return . fmap h to be a bit easier to read and understand.

David Turner

Actually, a couple of other ideas too. First, why not just use .:? Parsers themselves are instances of Alternative so you can use <|> to combine two parsers where if the first fails then the second is tried.

Secondly, I’d recommend not naming all the intermediate results and then combining them at the bottom. Too many times have I come across a bug where there was one missing in the final list! I can’t remember off the top of my head whether there’s a choice operator here as there is in e.g. attoprasec, but if there isn’t there should be!

David Turner

So here’s how I would write this function:

instance FromJSON VersionRange where
  parseJSON = withObject "VersionRange" $ \o -> msum
    [ nullaryOp anyVersion            <$> o .: "AnyVersion"
    , thisVersion                     <$> o .: "ThisVersion"
    , laterVersion                    <$> o .: "LaterVersion"
    , earlierVersion                  <$> o .: "EarlierVersion"
    , WildcardVersion                 <$> o .: "WildcardVersion"
    , binaryOp unionVersionRanges     =<< o .: "UnionVersionRanges"
    , binaryOp intersectVersionRanges =<< o .: "IntersectVersionRanges"
    , VersionRangeParens              <$> o .: "VersionRangeParens"
    ] where nullaryOp :: a -> Value -> a
            nullaryOp = const

            binaryOp :: Monad m => (a -> a -> b) -> [a] -> m b
            binaryOp f [a,b] = return $ f a b
            binaryOp _ _     = fail "binary operator did not have two arguments"

Никита Тимофеев

Perhaps it would be better to use a transformer MaybeT?

instance FromJSON V.VersionRange where
  parseJSON = withObject "VersionRange" go
    where
      go o = fromMaybe (typeMismatch "VersionRange" $ Object o) <$> runMaybeT $ do
        MaybeT ((o .:? "LaterVersion") >>= return . fmap V.laterVersion) <|>
        MaybeT ((o .:? "ThisVersion") >>= return . fmap V.thisVersion) <|>
        MaybeT ((o .:? "EarlierVersion") >>= return . fmap V.earlierVersion) <|>
        MaybeT ((o .:? "AnyVersion") >>= \ (_::Maybe [(Int,Int)]) -> return $ Just V.anyVersion) <|>
        MaybeT ((o .:? "WildcardVersion") >>= return . fmap V.WildcardVersion) <|>
        MaybeT ((o .:? "UnionVersionRanges") >>= return . fmap toUvr) <|>
        MaybeT ((o .:? "IntersectVersionRanges") >>= return . fmap toIvr) <|>
        MaybeT ((o .:? "VersionRangeParens") >>= return . fmap V.VersionRangeParens)

      toUvr [v0, v1] = V.unionVersionRanges v0 v1
      toIvr [v0, v1] = V.intersectVersionRanges v0 v1
Leave a comment