Reputation: 104105
I have a record I want to parse from JSON:
data ArticleInfo = ArticleInfo {
author :: String,
title :: String,
pubDate :: Day
} deriving (Show, Eq)
instance FromJSON ArticleInfo where
parseJSON (Object value) = ArticleInfo <$>
value .: "author" <*>
value .: "title" <*>
liftM parsePubDate (value .: "pubDate")
parseJSON _ = mzero
parsePubDate :: String -> Day
parsePubDate = parseTimeOrError True defaultTimeLocale "%Y-%-m-%-d"
parseArticleInfo :: String -> Maybe ArticleInfo
parseArticleInfo source = Data.Yaml.decode (pack source)
This works, but crashes on malformed dates that don’t fit the "%Y-%-m-%-d"
format specifier. The parser specification is above my head at the moment – what do I write there to make the ArticleInfo
parser return Nothing
when encountering such dates?
(I know I should start by replacing parseTimeOrError
with parseTime
, but that’s about it. And even parseTime
is deprecated, telling me to use parseTimeM
whose signature I don’t understand yet.)
Upvotes: 1
Views: 125
Reputation: 129119
If you’re okay with making the whole parser fail if there’s an invalid date, you can just thread that in without too much hassle:
parseJSON (Object value) = ArticleInfo <$>
value .: "author" <*>
value .: "title" <*>
(value .: "pubDate" >>= parseTimeM True defaultTimeLocale "%Y-%-m-%-d")
If, instead, you want a parser for Maybe ArticleInfo
(which I thought at first, but now realize you probably don’t want), read on…
First off, if you want to be parsing a Maybe ArticleInfo
, your FromJSON
instance is going to need to be for a Maybe ArticleInfo
, not an ArticleInfo
. (Doing this will require the language extension FlexibleInstances
.) Secondly, you are going to need to change your parsePubDate
to return a Maybe Day
as well, and replace parseTimeOrError
with parseTimeM
. (Indeed, when parseTimeM
is specialized with m ~ Monad
, parseTimeM
functions identically to the deprecated parseTime
.)
Then things get a little more complicated. Somewhat verbosely, you could do:
parseJSON (Object value) = (\a t pd -> ArticleInfo a t <$> pd) <$>
value .: "author" <*>
value .: "title" <*>
liftM parsePubDate (value .: "pubDate")
…but that’s a little repetitive and isn’t a very ‘nice’ solution. You could alternatively use the MaybeT
monad transformer, which might be a bit nicer:
parseJSON (Object value) =
runMaybeT $ ArticleInfo <$> lift (value .: "author")
<*> lift (value .: "title")
<*> (MaybeT $ parsePubDate <$> value .: "pubDate")
Also, all this makes Data.Yaml.decode (pack source)
return a Maybe (Maybe ArticleInfo)
. You can use join
to fold the Maybe
s together.
Upvotes: 2
Reputation: 5325
This is a typical use of the Maybe
monad. It's not that obvious to a beginner, but once you grok monads it becomes second nature.
parsePubDate :: String -> Maybe Day
parsePubDate = parseTimeM True defaultTimeLocale "%Y-%-m-%-d"
Upvotes: 2