More Context for Failures (Data.Aeson)

I had a problem last night where I was parsing a lot of non-trivial and annoyingly inconsistent json. These json documents have a very bad habit of replacing lists with single objects when a particular document contains a singleton list, which is maddening, but I can work around it when I know to expect it. That's just one example of the kinds of failures due to inconsistent documents. My parsing code looks like this: (Data.Aeson, btw) instance FromJSON Whatever where parseJSON (Object o) = do a <- parseSomething o b <- parseSomethingElse o c <- ... andManyMore <- ... return $ Whatever a b c ... parseJSON _ = fail "dude" In my code I might have 10 sub parsers so any of them might fail. When the parser fails I see a failure message like "expected [a] but got Object" and no additional context. And of course if I was in a language with a stack I could just look at the stack. (And I'd be miserable in other ways. So I'm not exactly complaining...) These documents are long enough that I burned a lot of time comparing Aeson dumps looking for where I got an object instead of an array. Wouldn't it be neato if I could do this... instance FromJSON Whatever where parseJSON (Object o) = do a <- failureContext "branchA" $ parseSomething o b <- failureContext "branchB" parseSomethingElse o return $ Whatever a b parseJSON _ = fail "dude" Now when it dies, the error message will be "branchB: expected [a] but got Object". This seems like a useful utility. Does it already exist and I don't know it? Would it be easy to write? I'm a little bit out of my depth here. I've never tried to handle exceptions in Haskell before. Seems like it would be something like: failureContext :: (MonadIOSomething m) => String -> m a -> m a failureContext errorContextName = -- Catch exceptions and rewrite messages -- or wrap the exception in something and rethrow like Erlang or Java -- Darrin

On Thu, Feb 23, 2012 at 02:21:25PM -0500, Darrin Thompson wrote:
Wouldn't it be neato if I could do this...
instance FromJSON Whatever where parseJSON (Object o) = do a <- failureContext "branchA" $ parseSomething o b <- failureContext "branchB" parseSomethingElse o return $ Whatever a b parseJSON _ = fail "dude"
Now when it dies, the error message will be "branchB: expected [a] but got Object".
What immediately comes to mind would be to use ReaderT [String] along with the 'local' method to maintain an explicit stack of parsing contexts. However, parseJSON is not polymorphic in the monad used -- so there's no way to use ReaderT in the implementation of parseJSON methods. However, parseJSON returns a 'Parser' which is defined like this: -- | Failure continuation. type Failure f r = String -> f r -- | Success continuation. type Success a f r = a -> f r -- | A continuation-based parser type. newtype Parser a = Parser { runParser :: forall f r. Failure f r -> Success a f r -> f r } So I think it is possible to leverage the "failure continuation" to do what you want. In particular, failureContext :: String -> Parser a -> Parser a failureContext branch p = Parser (\f s -> runParser p (f . (++ (branch ++ ": "))) s) Untested but I'm pretty sure it will work. The idea is that 'failureContext' transforms a parser by modifying its "failure continuation" to first tack the current branch onto the front of the error message. These can be stacked just like you would expect. Argh, I just realized that this *would* work except that Data.Aeson.Types.Internal does not export the Parser constructor! In fact, the aeson package also does not export the Data.Aeson.Types.Internal module at all. So to make this work you would have to build your own customized version of aeson. This is not too difficult (cabal unpack aeson; cd aeson; export more stuff; bump version number in cabal file; cabal install) but if this is something you want to release then obviously you can't have it depending on a customized version of aeson. If it's just an internal tool though, this may work fine. Of course, you can also submit a patch to the aeson maintainer adding the relevant exports. -Brent

On Thu, Feb 23, 2012 at 02:51:57PM -0500, Brent Yorgey wrote:
failureContext :: String -> Parser a -> Parser a failureContext branch p = Parser (\f s -> runParser p (f . (++(branch ++ ": "))) s)
Whoops, of course that should be ((branch ++ ": ") ++) instead of (++ (branch ++ ": ")) to put the branch tag on the beginning instead of the end of the message. -Brent
participants (2)
-
Brent Yorgey
-
Darrin Thompson