|
| 1 | +# Improve `EncodingError` and `DecodingError`'s printed descriptions |
| 2 | + |
| 3 | +* Proposal: [SE-NNNN](NNNN-codable-error-printing.md) |
| 4 | +* Authors: [Zev Eisenberg](https://github.com/ZevEisenberg) |
| 5 | +* Review Manager: TBD |
| 6 | +* Status: **Awaiting review** |
| 7 | +* Implementation: https://github.com/swiftlang/swift/pull/80941 |
| 8 | +* Review: TBD |
| 9 | + |
| 10 | +## Introduction |
| 11 | + |
| 12 | +`EncodingError` and `DecodingError` do not specify any custom debug description. The default descriptions bury the useful information in a format that is difficult to read. Less experienced developers may assume they are not human-readable at all, even though they contain useful information. The proposal is to conform `EncodingError` and `DecodingError` to `CustomDebugStringConvertible` and provide nicely formatted debug output. |
| 13 | + |
| 14 | +## Motivation |
| 15 | + |
| 16 | +Consider the following example model structs: |
| 17 | + |
| 18 | +```swift |
| 19 | +struct Person: Codable { |
| 20 | + var name: String |
| 21 | + var home: Home |
| 22 | +} |
| 23 | + |
| 24 | +struct Home: Codable { |
| 25 | + var city: String |
| 26 | + var country: Country |
| 27 | +} |
| 28 | + |
| 29 | +struct Country: Codable { |
| 30 | + var name: String |
| 31 | + var population: Int |
| 32 | +} |
| 33 | +``` |
| 34 | + |
| 35 | +Now let us attempt to decode some invalid JSON. In this case, it is missing a field in a deeply nested struct. |
| 36 | + |
| 37 | +```swift |
| 38 | +// Note missing "population" field |
| 39 | +let jsonData = Data(""" |
| 40 | +[ |
| 41 | + { |
| 42 | + "name": "Ada Lovelace", |
| 43 | + "home": { |
| 44 | + "city": "London", |
| 45 | + "country": { |
| 46 | + "name": "England" |
| 47 | + } |
| 48 | + } |
| 49 | + } |
| 50 | +] |
| 51 | +""".utf8) |
| 52 | + |
| 53 | +do { |
| 54 | + _ = try JSONDecoder().decode([Person].self, from: jsonData) |
| 55 | +} catch { |
| 56 | + print(error) |
| 57 | +} |
| 58 | +``` |
| 59 | + |
| 60 | +This outputs the following: |
| 61 | + |
| 62 | +`keyNotFound(CodingKeys(stringValue: "population", intValue: nil), Swift.DecodingError.Context(codingPath: [_CodingKey(stringValue: "Index 0", intValue: 0), CodingKeys(stringValue: "home", intValue: nil), CodingKeys(stringValue: "country", intValue: nil)], debugDescription: "No value associated with key CodingKeys(stringValue: \"population\", intValue: nil) (\"population\").", underlyingError: nil))` |
| 63 | + |
| 64 | +All the information you need is there: |
| 65 | +- The kind of error: a missing key |
| 66 | +- Which key was missing: `"population"` |
| 67 | +- The path of the value that had a missing key: index 0, then key `"home"`, then key `"country"` |
| 68 | +- The underlying error: none, in this case |
| 69 | + |
| 70 | +However, it is not easy or pleasant to read such an error, particularly when dealing with large structures or long type names. It is common for newer developers to assume the above output is some kind of log spam and not even realize it contains exactly the information they are looking for. |
| 71 | + |
| 72 | +## Proposed solution |
| 73 | + |
| 74 | +Conform `EncodingError` and `DecodingError` to `CustomDebugStringConvertible` and provide a clean, readable debug description for each. Here is an example of the proposed change for the same decoding error as above: |
| 75 | + |
| 76 | +``` |
| 77 | +Key 'population' not found in keyed decoding container. |
| 78 | +Debug description: No value associated with key CodingKeys(stringValue: "population", intValue: nil) ("population"). |
| 79 | +Path: [0]/home/country |
| 80 | +``` |
| 81 | + |
| 82 | +(Note: the output could be further improved by modifying `JSONDecoder` to write a better debug description. See [Future Directions](#future-directions) for more.) |
| 83 | + |
| 84 | +### Structure |
| 85 | + |
| 86 | +1. Description using information we know from the associated values of the error enum itself. |
| 87 | +1. The debug description that was passed to the error, if it is not empty. |
| 88 | +1. The underlying error, if it is non-nil. |
| 89 | +1. The coding path, neatly formatted, if it is non-empty. String keys are presented as-is, and numeric indices are presented in square brackets like `[2]` to differentiate them from string keys. |
| 90 | + |
| 91 | +More complete examples of the before/after diffs are available in the description of the pull request: https://github.com/swiftlang/swift/pull/80941. |
| 92 | + |
| 93 | +The path formatting is especially improved. Comparing the examples from above: |
| 94 | + |
| 95 | +```diff |
| 96 | +-[_CodingKey(stringValue: "Index 0", intValue: 0), CodingKeys(stringValue: "home", intValue: nil), CodingKeys(stringValue: "country", intValue: nil)] |
| 97 | ++Path: [0]/home/country |
| 98 | +``` |
| 99 | + |
| 100 | +## Detailed design |
| 101 | + |
| 102 | +```swift |
| 103 | +@available(SwiftStdlib 6.2, *) |
| 104 | +extension EncodingError: CustomDebugStringConvertible { |
| 105 | + public var debugDescription: String {...} |
| 106 | +} |
| 107 | + |
| 108 | +@available(SwiftStdlib 6.2, *) |
| 109 | +extension DecodingError: CustomDebugStringConvertible { |
| 110 | + public var debugDescription: String {...} |
| 111 | +} |
| 112 | +``` |
| 113 | + |
| 114 | +## Source compatibility |
| 115 | + |
| 116 | +The new conformance changes the result of converting an `EncodingError` or `DecodingError` value to a string. This changes observable behavior: code that attempts to parse the result of `String(describing:)` or `String(reflecting:)` can be misled by the change of format. |
| 117 | + |
| 118 | +However, the documentation of these interfaces explicitly state that when the input type conforms to none of the standard string conversion protocols, then the result of these operations is unspecified. |
| 119 | + |
| 120 | +Changing the value of an unspecified result is not considered to be a source incompatible change. |
| 121 | + |
| 122 | +## ABI compatibility |
| 123 | + |
| 124 | +The proposal retroactively conforms two previously existing standard types to a previously existing standard protocol. This is technically an ABI breaking change: on ABI-stable platforms, we may have preexisting Swift binaries that assume that `EncodingError is CustomDebugStringConvertible` or `DecodingError is CustomDebugStrinConvertible` returns `false`, or ones that are implementing this conformance on their own. |
| 125 | + |
| 126 | +We do not expect this to be an issue in practice. |
| 127 | + |
| 128 | +## Implications on adoption |
| 129 | + |
| 130 | +[Unsure what to add here. I see stuff in [SE-0445](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0445-string-index-printing.md#implications-on-adoption), but I'm not sure how much of that applies here. I don't know if I need to be doing the `@backDeployed` things that proposal mentions, and when I look at the code from the PR, I see `@_alwaysEmitIntoClient // FIXME: Use @backDeployed`.] |
| 131 | + |
| 132 | +## Future directions |
| 133 | + |
| 134 | +### Better error generation from Foundation encoders/decoders |
| 135 | + |
| 136 | +The debug descriptions generated in Foundation sometimes contain the same information as the new debug descriptions from this proposal. A future change to the standard JSON and Plist encoders and decoders could provide more compact debug descriptions once they can be sure they have the new standard library descriptions available. They could also use a more compact description when rendering the description of a `CodingKey`. Using part of the example from above: |
| 137 | + |
| 138 | +``` |
| 139 | +Debug description: No value associated with key CodingKeys(stringValue: "population", intValue: nil) ("population"). |
| 140 | +``` |
| 141 | + |
| 142 | +The `CodingKeys(stringValue: "population", intValue: nil) ("population")` part is coming from the default `description` of `CodingKey`, plus an extra parenthesized string value at the end for good measure. The Foundation (de|en)coders could construct a more compact description that does not repeat the key, just like we do within this proposal in the context of printing a coding path. |
| 143 | + |
| 144 | +### Print context of surrounding lines in source data |
| 145 | + |
| 146 | +When a decoding error occurs, in addition to printing the path, the error message could include some surrounding lines from the source data. This was explored in this proposal's antecedent, [UsefulDecode](https://github.com/ZevEisenberg/UsefulDecode). But that requires passing more context data from the decoder and changing the public interface of `DecodingError` to carry more data. This option is probably best left as something to think about as [we design `Codable`'s successor](https://forums.swift.org/t/the-future-of-serialization-deserialization-apis/78585). But just to give an example of the _kind_ of context that could be provided (please do not read anything into the specifics of the syntax; this is a sketch, not a proposal): |
| 147 | + |
| 148 | +``` |
| 149 | +Value not found: expected 'name' (String) at [0]/address/city/birds/[1]/name, got: |
| 150 | +{ |
| 151 | + "feathers" : "some", |
| 152 | + "name" : null |
| 153 | +} |
| 154 | +``` |
| 155 | + |
| 156 | +## Alternatives considered |
| 157 | + |
| 158 | +The original version of this proposal suggested conforming `EncodingError` and `DecodingError` to `CustomStringConvertible`, not `CustomDebugStringConvertible`. The change to the debug-flavored protocol emphasizes that the new descriptions aren't intended to be used outside debugging contexts. This is in keeping with the precedent set by [SE-0445](0445-string-index-printing.md). |
| 159 | + |
| 160 | +The original version also proposed changing `CodingKey.description` to return the bare string or int value, but changing the exsting implementation of an existing public method was deemed too potentially dangerous. |
| 161 | + |
| 162 | +In terms of formatting, we could do away with the square brackets around integers and just interpolate them in directly: |
| 163 | + |
| 164 | +```diff |
| 165 | +-path/to/thing/[2]/[4]/more/stuff |
| 166 | ++path/to/thing/2/4/more/stuff |
| 167 | +``` |
| 168 | + |
| 169 | +## Acknowledgments |
| 170 | + |
| 171 | +This proposal lifts large portions almost verbatim from [SE-0445](0445-string-index-printing.md). Thanks to [Karoy Lorentey](https://github.com/lorentey) for writing that proposal, and for flagging it as similar to this one. |
| 172 | + |
| 173 | +Thanks to Kevin Perry [for suggesting](https://forums.swift.org/t/the-future-of-serialization-deserialization-apis/78585/77) that this would make a good standalone change regardless of the direction of future serialization tools, and for engaging with the PR from the beginning. |
0 commit comments