Good error messages when parsing JSON

In the project I’m currently working on, we do an extensive usage of aeson, and specifically its generic FromJSON instance and deriveJSON.

We are quite not satisfied by the quality of error messages produces by such instances, in particular when there are nested objects.

I took a look around and I found aeson-better-errors: Better error messages when decoding JSON values., which provides great error messages but requires hand-written parsers.

Is there any other alternative approach which combines top-notch error messages without having to manually write our own parsers?

2 Likes

It might be helpful to see what you consider poor error messages. Can you provide an example error message and an accompanying parser that produces such a message, and maybe say what you think a good error would be?

One error for example is "InvalidArgs "parsing Int64 failed, expected Number, but encountered Null", which provides no information about which field was not correct.

We don’t have hand-written parsers, we are either using the generic implementation or the template Haskell version generated with deriveJSON. One solution to the whole issue could be just manually writing our own parsers.

A good error message would provide details on the path of the field where the parsing error happened.

I just wanted to collect ideas about possible ways to improve our current situation.

3 Likes

I still think it would be good to see what code produces this because I could have sworn that aeson already had field/parse location tracking. Would you be able to put something together as a starting point?

Let me post a concrete example.

Given these datatypes and FromJSON instances

data PersonTQ = PersonTQ
  { ptqType :: PersonT
  , ptqQuantity :: Int
  }
  deriving (Eq, Show, Generic)

instance FromJSON PersonTQ

data BookingFormResult = BookingFormResult
  { forder :: !(Maybe BookingFormOrder)
  , orderId :: !(Maybe OrderId)
  , persons :: [PersonTQ]
  , start :: !UTCTime
  , end :: !(Maybe UTCTime)
  , timeZone :: !(Maybe String)
  , prod :: !Int64
  , platform :: !Platform
  , foreignId :: !Text
  , language :: !Language
  , bfAddons :: [AddonBF]
  }
  deriving (Eq, Show, Generic)

instance FromJSON BookingFormResult

when we receive the following json

{
  "timeZone": null,
  "start": "2023-08-25T07:00:00Z",
  "prod": 569,
  "platform": "BlueMoon",
  "persons": [
    {
      "ptqType": "Adults",
      "ptqQuantity": 0.24 -- this is the issue, should be an integer!
    }
  ],
  "orderId": null,
  "language": "English",
  "foreignId": "",
  "forder": {
    "phonet": "",
    "phone": "",
    "lname": "zucon",
    "fname": "gigi",
    "email": "",
    "cusnotes": null
  },
  "end": null,
  "bfWpAddons": [],
  "bfAddons": []
}

the error message we get is

Error: parsing Int failed, value is either floating or will cause over or underflow 0.24

The same happens using

$(deriveJSON defaultOptions ''PersonTQ)

$(deriveJSON defaultOptions ''BookingFormResult)

I don’t consider this a aeson bug, but still I’d like to improve the situation of my codebase

1 Like

I might consider that an aeson bug, given that aeson specifically has a type to track “paths” through JSON decoding: Data.Aeson.Types. And as the documentation for <?> states:

(Standard methods like (.:) already do this.)

So I really do think you should be getting at least something like: $.persons.0.ptqQuantity: Error: Parsing Int failed...

2 Likes

aeson-warning-parser: Library providing JSON parser that warns about unexpected fields in objects. may be of interest. It is used by Stack/Pantry, and was recently spun out of Pantry.

Which version of Aeson are you using? I get the good error messages that @ocharles expects with both 2.1.x and 2.2.x.

2 Likes

Ah, that must be the reason! We’re still on aeson 2.0.

I tried to update some days ago but stackage was down. I’ll try again tomorrow

2 Likes

I did try with aeson 2.1.2.1 (I couldn’t try 2.2.0.0 due to incompatibilities with other dependencies in my project) but nothing changed.

So I tried to debug the issue more carefully.

First I noticed that using eitherDecode in ghci I was in fact getting a decent error message.

Therefore I dug deeper into the aeson implementation and found out that eitherDecode is using iparse, which preserves information about the path of the error, while fromJSON is using parse, which on the other hand forgets the path of the error.

As a matter of fact, yesod-core is using fromJSON and that’s why the API returns a poor error message

3 Likes

Good find!

Perhaps Yesod could use Data.Aeson.Types.ifromJson instead, which returns the JSON path to the error.

already opened an issue! Error message quality: `parse` vs `iparse` · Issue #1057 · haskell/aeson · GitHub

3 Likes