zoul
zoul

Reputation: 104105

JSON parsing in Haskell: Fail on malformed value

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

Answers (2)

icktoofay
icktoofay

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 Maybes together.

Upvotes: 2

NovaDenizen
NovaDenizen

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

Related Questions