Learn Courses My Dashboard

Creating user error message for JSON decoding error

I’m testing error code when decoding JSON data. I changed the CodingKeys so I would get a 'KeyNotFound error from JSON decode. I have this code to catch the error:

catch let error
{
  print(error.localizedDescription)
  print(error)
}
  • The first print shows The data couldn't be found because it is missing.
  • The second print shows keyNotFound(<lots of debugging info...>

The first message is wrong, the second is not user-friendly. What can I do to get an error message suitable for a user alert?

You don’t need “catch let error” just put catch and error will automatically be there.

Also, Write your own error message for the user.

They shouldn’t hardly ever see error messages that are generated

how can you say that the first message is wrong? if thats the description then maybe there was an issue like you didn’t “add to target” the JSON file when you added it to your project

Removed ‘let error’ and no compiler error. That’s interesting, all the examples I saw had ‘let error’. Thanks Mike, the less code, the better, I always say.

I thought the purpose of localiazedDescription was so you don’t have to write your own messages. Also, I would have to have more code to check for each possible error code. See above re: the less code, the better.

The JSON is fine. I purposely changed the coding keys to force a ‘key not found’ error… I know the first one is wrong because it disagrees with the second one, which is correct.

You can do something like this:

let json = """
{
    "key1": "value1",
    "key2": "value2"
}
"""

struct MyType: Decodable {
    let key1: String
    
    enum CodingKeys: String, CodingKey {
        case key1 = "keyOne"
    }
}

let decoder = JSONDecoder()
do {
    let _ = try decoder.decode(MyType.self, from: json.data(using: .utf8)!)
}
catch let DecodingError.keyNotFound(_, context){
    print(context.debugDescription)
    //prints "No value associated with key CodingKeys(stringValue: "keyOne", intValue: nil) ("keyOne")."
}
catch {
    print(error.localizedDescription)
}

The first catch will trap on DecodingError.keyNotFound and then you can get more detailed information about the specific key causing the error.

Any other errors besides keyNotFound will fall through to the second catch and you can then just use the localizedDescription or whatever.

I guess I didn’t make it clear that I wanted a message that I can put into an alert, so the user knows what happened. The print() was only so I could see what each message looked like.

My issue is that the message is wrong, but I guess there’s nothing I can do about that.

If you post the entire code that deals with the URLSession and the JSON decoding we might be able to make sense of what you are trying to do. Posting code snippets without context makes it difficult to advise.

There is not going to be anything the system gives you that will be 100% user-friendly but you can construct a suitable message given the suggestions offered in this thread.

For instance, taking my own suggestion, you can look up the documentation for DecodingError and see that the parameter I ignored with _ is actually the offending key, which has a property called stringValue. So…

catch let DecodingError.keyNotFound(key, _) {
    let userMessage = "Key not found: \(key.stringValue)"
    //result in the message "Key not found: keyOne"
}

Then you can present that message to the user however you want.

And, really, from what you’ve told us, the localizedDescription isn’t wrong, it’s just vaguely worded. If a key cannot be not found because you altered the CodingKeys, then truly that data is missing. The CodingKeys tell the system what data you expect to be there and if there’s not a match in the incoming JSON… that data is missing.

But like Chris says, more code would be useful. It’s hard to diagnose and offer a proper fix with only a single catch clause provided. Please show us at least a JSON sample and your struct for decoding it.

2 Likes

This is not good thinking. Less code is not always better, do whatever is most readable and easily understandable.

It was kind of a tongue-in-cheek comment. What I meant was, you shouldn’t have unnecessary
code. In this case, why have a bunch of catch blocks to display a message for each condition when the proper message is already provided?

1 Like

OK, here’s all the code that deals with reading and writing the data:

  // Encode and save the data.

  func save()
  {
    let encoder = JSONEncoder()

    items.sort()
    { $0.dueDate < $1.dueDate }

    if let encoded = try? encoder.encode(items)
    { saveFile(data: encoded) }
  }

  func load()
  {
    if let json = loadFile()
    {
      let decoder = JSONDecoder()

      do
      {
        let decoded = try decoder.decode([Periodical].self, from: json)
        items = decoded
        return
      }
      catch 
      {
        print(error.localizedDescription)
        print(error)
        errorHeading = "Error decoding data"
        appError = AppError(errorString: error.localizedDescription)
      }
    }

    items = []
  }

  // Return the URL of the app's documents directory.

  private func getDocumentsDirectory() -> URL
  {
    // Find all possible documents directories for this user.
    let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)

    // Just return the first one, which ought to be the only one.
    return paths[0]
  }

  private func saveFile(data: Data)
  {
    let url = getDocumentsDirectory().appendingPathComponent(Self.itemKey + Self.fileExt)

    do
    { try data.write(to: url, options: [.atomicWrite, .completeFileProtection]) }
    catch
    {
      print(error.localizedDescription)
      print(error)
      errorHeading = "Error saving data"
      appError = AppError(errorString: error.localizedDescription)
    }
  }

  private func loadFile() -> Data?
  {
    let url = getDocumentsDirectory().appendingPathComponent(Self.itemKey + Self.fileExt)

    if !FileManager.default.fileExists(atPath: url.relativePath)
    { return nil }

    do
    {
      let data = try Data(contentsOf: url)
      return data
    }
    catch
    {
      print(error.localizedDescription)
      print(error)
      errorHeading = "Error reading data"
      appError = AppError(errorString: error.localizedDescription)
      return nil
    }
  }

…and here:s the code that displays the alert:

    .alert(item: $periodicals.appError)
    { appAlert in
      Alert(title: Text(periodicals.errorHeading),
            message: Text(appAlert.errorString))
    }

I also tested it for a ‘file does not exist’ error and the localizedDescription is accurate:
“The file “Items2.txt” couldn’t be opened because there is no such file.”

So maybe I’m just being too picky. The ‘key not found’ situation should never occur anyway, unless there’s a bug. I think I’ll just go with the supplied messages unless someone has a better idea.

I appreciate this discussion. Always good to get other opinions and I alway learn something from them.