How do you read a DecodingError, and how do you decode tolerantly?

2.5.4 · L2 · Interview

Predict first

What does this print?

struct Item: Decodable { let id: Int }
struct Box: Decodable { let items: [Item] }
do {
    _ = try JSONDecoder().decode(Box.self, from: Data(#"{"items":[{"id":"x"}]}"#.utf8))
} catch DecodingError.typeMismatch(_, let ctx) {
    print(ctx.codingPath.map(\.stringValue))
}
  • ["items", "id"]
  • ["items", "Index 0", "id"]
  • ["id"]

Predict first

What does this print for a missing key?

struct Item: Decodable { let id: Int }
struct Box: Decodable { let items: [Item] }
do {
    _ = try JSONDecoder().decode(Box.self, from: Data(#"{"items":[{}]}"#.utf8))
} catch DecodingError.keyNotFound(let key, let ctx) {
    print(key.stringValue, ctx.codingPath.map(\.stringValue))
}
  • id ["items", "Index 0"]
  • id ["items", "Index 0", "id"]

Mental model. The coding path is a street address for the bug: building, floor, door.

Decoding failures throw a DecodingError with four cases: keyNotFound, typeMismatch, valueNotFound and dataCorrupted. Each carries a Context whose codingPath says exactly where it went wrong, for example ["items", "Index 0", "id"], plus a debugDescription. Log the path and the case, not the raw payload, which may contain personal data.

When a server is sloppy, make the model tolerant on purpose: decodeIfPresent for optional fields, defaults in a custom init(from:), and a wrapper that decodes an array element by element so one bad element is skipped instead of failing the whole list. Never wrap everything in try?: you lose the path that would have told you why.

The path walks the document: the items key, array element 0, then the id key.

For keyNotFound the path leads to the container that lacks the key, and the key is reported separately.

Common traps

  • try? everywhere. You turn a precise error into silence.
  • Logging the whole payload on failure. It can leak user data. Log the path.

They might follow up with

  • How does the path show array positions? As keys named Index 0, Index 1.
  • How do you test the unhappy path? Feed malformed fixtures and assert the case and the path.

Related questions

  • 2.5.1 What does keyDecodingStrategy do, and when does convertFromSnakeCase not match?
  • 2.5.5 When would you use JSONSerialization instead of Codable, and what does the untyped route cost?

← Which JSONEncoder and JSONDecoder options matter beyond keys and…   When would you use JSONSerialization instead of Codable, and what… →