Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to correctly error out in JSON parsing with Data.Aeson

My type and correponding FromJSON implementation as listed below.

The nonEmpty turns a List into a Maybe NonEmpty and I'm trying to correctly deal with the case where the List is indeed empty and I have to abort the parsing. This parsing is actually done inside of parseJsonBody, which means I don't want to error "foo" my way out of it, but I want to return mzero (or whatever else will do the trick, mzero is the only thing I've stumbled upon so far) so that the handler correctly returns a 400 instead of crashing with a 500.

The approach below compiles, but as far as I can tell is pretty much equals to error or some other form of exception throwing inside of the parseJSON. If I return mzero however (e.g. with <*> mzero instead of that line), it fails nicely as intended.

import qualified Data.List.NonEmpty as NE
data GSAnswer = GSAnswer { gsAnswerQuestionId :: Int
                         , gsAnswerResponses :: NE.NonEmpty GSResponse
                         } deriving (Show, Eq)


instance FromJSON GSAnswer where
  parseJSON (Object o) =
    GSAnswer <$> o .: "question-id"
             -- how do I return mzero here based on NE.nonEmpty?
             -- this will throw an exception right now on an empty list
             <*> fmap (fromMaybe (fail "foo") . NE.nonEmpty) (o .: "responses")
  parseJSON _ = mzero

One option would be to somehow pattern match on the result of fmap NE.nonEmpty (o .: "responses"), but I can't quite figure out what the pattern would be there: doesn't look like Parser has any constructors?

like image 311
Alexandr Kurilin Avatar asked Feb 13 '15 07:02

Alexandr Kurilin


1 Answers

Essentially, you need a Parser [a] -> Parser NE.NonEmpty transformer, which is relatively easy:

-- toNonEmptyP :: Parser [a] -> Parser NE.NonEmtpy
toNonEmptyP p = fmap NE.nonEmpty p >>= maybe mzero return

We map NE.nonEmpty on our regular list parser, which gives us Parser (Maybe NE.NonEmpty). We then inspect the Maybe with maybe and either use mzero if it was Nothing, or return the parsed value back to the parsing context. Your FromJSON instance then boils down to

instance FromJSON GSAnswer where
  parseJSON (Object o) =
    GSAnswer <$> o .: "question-id"
             <*> toNonEmptyP (o .: "responses")
  parseJSON _ = mzero

You can use fail msg instead of mzero to provide a custom error message, as fail :: String -> Parser a doesn't bottom out.

like image 192
Zeta Avatar answered Sep 28 '22 02:09

Zeta