Decodable - get reference to original position in JSON

I am using JSONDecoder to produce a nested set of objects from a JSON string. Is there a way, in the init(from decoder: Decoder) implementation on a nested class, to get a reference to where in the original JSON string it originates? This could be line number and column. Or equally useful to get the actual JSON snippet.

I have two use cases for this. Most important is when I use a trick to get "safe" decoding of arrays, like the Safe type described here. The method is great to ensure we do not fail the entire decoding but only the one object in a large array. The limitation is that it is quite difficult to diagnose which object specifically failed.

Another use case is to keep a reference to the "source" in decoded objects.

Hello,

Your implementation of Safe could store a Result instead of an optional success value. In the result, store the DecodingError that prevented the value from being decoded in the first place.

From the set of Safe decoded objects, the ones that contains a failure Result will tell you everything that Swift decoding system can tell, with the DecodingError.Context value that is attached to all kinds of DecodingError. The context doesn't tell you anything about the physical location in the original JSON data. Instead, it tells you the codingPath, an array of JSON object keys and array indexes. From this path, you can build a physical location in the original JSON data.

Look at this gorgeous screenshot from Pulse 2.0, where the erroneous value is highlighted in JSON text. There's no doubt DecodingError gives you the information you need.

Also note how the maintainer of Pulse is the same @kean who wrote the blog post you are referring to ;-) Alex rocks :+1:

1 Like

That looks really cool, no doubt. Going via the DecodingError.Context as you suggest is certainly a viable way and I guess I can look at Pulse source to see how they do it.

I assume I would need to parse the JSON again using something like NSJSONSerialization (JSONSerialization in Swift) in order to make sense of the CodingPath, or perhaps better a streaming decoder for JSON.

This seems like a lot of code to get to a context that is already there in the JSONDecoder, although hidden it seems.

Yes, it's probably a lot of code. And I'm not even sure NSJSONSerialization will give you the physical location you're after :sweat_smile: Definitely, looking at the Pulse source will give hints.

The encoding/decoding system is designed to work with any kind of serialization (JSON, Property lists, database rows, whatever), and that's why DecodingError does not expose anything that is specific to the underlying physical storage. Consider a database row, for example: it does not exist as a flat buffer of bytes, but rather some kind of dictionary, or a bunch of index or key-based functions.

Thanks for the shoutout, @gwendal.roue!

The code for rendering JSON in Pulse isn't pretty, but it gets the job done.

It uses JSONDecoder to try and decode the data and record an error if decoding fails. DecodingError contains a codingPath (an array of CodingKey values). A key can be either a String (in case it's an object) or an Int (in case it's an array). By using the coding path, it's easy to find the values that resulted in an error.

I don't map the error back to the original file and the exact line or position in Pulse. I use JSONSerialization.jsonObject(with:options:) to create dynamic json objects (Any) and then render them recursively while remembering the coding path. If the current coding path matches the coding path from the error, I highlight the rendered text. Finding the position in the original file is going to be significantly more complicated.

2 Likes

P.S. yes, @gahms, sounds like you need Pulse :smiley: