35

I have the following code:

import Foundation

let jsonData = """
[
    {"firstname": "Tom", "lastname": "Smith", "age": "28"},
    {"firstname": "Bob", "lastname": "Smith"}
]
""".data(using: .utf8)!

struct Person: Codable {
    let firstName, lastName: String
    let age: String?

    enum CodingKeys : String, CodingKey {
        case firstName = "firstname"
        case lastName = "lastname"
        case age
    }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        firstName = try values.decode(String.self, forKey: .firstName)
        lastName = try values.decode(String.self, forKey: .lastName)
        age = try values.decode(String.self, forKey: .age)
    }

}

let decoded = try JSONDecoder().decode([Person].self, from: jsonData)
print(decoded)

Problem is it's crashing on age = try values.decode(String.self, forKey: .age). When I take that init function out it works fine. The error is No value associated with key age (\"age\")..

Any ideas on how to make that optional and not have it crash when it doesn't exist? I also need that init function for other reasons, but just made a simple example to explain what is going on.

Lukas Würzburger
  • 6,543
  • 7
  • 41
  • 75
Charlie Fish
  • 18,491
  • 19
  • 86
  • 179
  • 7
    Compare [What is difference between optional and decodeIfPresent when using Decodable for JSON Parsing?](https://stackoverflow.com/questions/46292325/what-is-difference-between-optional-and-decodeifpresent-when-using-decodable-for). – Martin R Oct 13 '17 at 11:03
  • @MartinR very helpful! Thanks so much. – Charlie Fish Oct 13 '17 at 11:08

3 Answers3

110

Age is optional:

let age: String? 

So try to decode in this way:

let age: String? = try values.decodeIfPresent(String.self, forKey: .age)
Oleg Gordiichuk
  • 15,240
  • 7
  • 60
  • 100
1

In my model, what I did is

var title: String?

...

title = try container.decode(String?.self, forKey: .age)

rather than

name = try? container.decode(String.self, forKey: .age)

For example for some reason backend passes a numeric value for that key, like title: 74. With the 1st method, you will get error message like this (I wrote a unit test for my model), and you can pinpoint the problem quickly

enter image description here

However, With the 2nd method try?, this useful error message will be silenced and will not bubble up. You will still get a valid decoding result title = nil, which actually is not true in most cases, if only null from backend is supposed to yield a nil.

But of course, if you design is that no matter what is retrieved from backend, as long as the type does not match, it will be a nil. Then I think there shouldn't be any problem with the 1st method.

infinity_coding7
  • 434
  • 5
  • 16
  • 2
    you can also try to use try container.decodeIfPresent(String.self, forKey: .age), this is the safe way to handle it. – Alirza Eram Aug 17 '22 at 06:05
-5

This is work for me.

var age: String? = nil
age = try? container.decode(String.self, forKey: .age)

Hope for help.

Jerome
  • 2,114
  • 24
  • 24
  • 16
    This is wrong, `try?` just ignores any errors without addressing the real issue, `decodeIfPresent` should be used instead. – maxkonovalov Apr 18 '18 at 16:41
  • `throws: DecodingError.typeMismatch if the encountered encoded value` – user25917 Nov 01 '18 at 10:08
  • @user25917 - that's something else. that line of code will _never_ throw an error since the try? just resolves to nil instead of throwing in this case. look up how swift do try catch error handling works if you still are confused. – LightningStryk Apr 23 '20 at 21:34