When decoding a Codable struct from JSON, how do you initialize a property not present in the JSON?

Say that in this example here, this struct

struct Reminder: Identifiable {
    var id: String = UUID().uuidString
    var title: String
    var dueDate: Date
    var notes: String? = nil
    var isComplete: Bool = false
}

is instead decoded from JSON array values (rather than constructed like in the linked example). If each JSON value were to be missing an "id", how would id then be initialized? When trying this myself I got an error keyNotFound(CodingKeys(stringValue: "id", intValue: nil), Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: \"id\", intValue: nil) (\"id\").", underlyingError: nil)).

But why?!

I think these 2 tutorials/documentation can help you with this problem (and other related problems too):

https://developer.apple.com/documentation/foundation/archives_and_serialization/using_json_with_custom_types

https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types

The sample code is broken for me (FB10028043), but I'll take a look at the article.

I still don't get why you want complicate things by not providing ids in json (e.g. "1", "2", "3", could do). If that's still a problem here's a possible simple solution:

struct Reminder: Identifiable, Codable {
    var id: String { _id! }
    private var _id: String?
    var title: String
    var dueDate: Date
    var notes: String? = nil
    var isComplete: Bool = false
    
    mutating func prepare() {
        _id = _id ?? UUID().uuidString
    }
}

func test(_ data: Data) {
    var reminders = try! JSONDecoder().decode([Reminder].self, from: data)
    for i in 0 ..< reminders.count {
        reminders[i].prepare()
    }
    print(reminders)
    model.state.reminders = reminders // safe to use as all ID's are in there
}

If you do actually want to follow that example exactly and default to having id be generated as UUID().uuidString every time (which, keep in mind, will give you a new UUID every single time you decode a Reminder struct — even the same one repeatedly) then this can be achieved as easily as leaving out id from Reminder's CodingKeys:

struct Reminder: Identifiable, Decodable {
    var id: String = UUID().uuidString
    var title: String
    var dueDate: Date
    var notes: String? = nil
    var isComplete: Bool = false
    
    private enum CodingKeys: String, CodingKey {
        case title, dueDate, notes, isComplete
    }
}

This will avoid attempting to decode id from a Decoder at all, which will always give it the assigned default value.

If you'd prefer id to be assigned UUID().uuidString only if id is not found in the JSON, you can achieve this by overriding init(from:):

struct Reminder: Identifiable, Codable {
    var id: String = UUID().uuidString
    var title: String
    var dueDate: Date
    var notes: String? = nil
    var isComplete: Bool = false
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
        title = try container.decode(String.self, forKey: .title)
        dueDate = try container.decode(Date.self, forKey: .dueDate)
        notes = try container.decodeIfPresent(String.self, forKey: .notes)
        isComplete = try container.decode(Bool.self, forKey: .isComplete)
    }
}

Two things to note:

  1. In this second example, I made Reminder Codable instead of just Decodable so the compiler would synthesize CodingKeys for us (since it's synthesizing encode(to:)). If you want it to just be Decodable and you implement init(from:) manually, you'll need to also define CodingKeys manually
  2. Both in the original example, and here, isComplete will also throw an error if not found in the JSON, since it's not optional — even though it has a default value. If you want to fall back to false if not present, you'll similarly need to change isComplete = ... to use decodeIfPresent and fall back to false
3 Likes

Huh, I wonder how it got wrapped in an extra .playground package. Thanks for reporting it. In the mean time... you can get at the original with 'Show Package Contents' and opening the nested playground.

Right direction but as written it won't work: decoder won't call the init: for starters it doesn't know which init (of possibly few) to call. You want this:

struct Reminder: Identifiable, Codable {
    let id: UUID = UUID() // Warning, see below
    var title: String
    var dueDate: Date
    var notes: String? = nil
    var isComplete: Bool = false
}

which has an added benefit of not having the boilerplate, yet at the same time with a drawback of this new warning: "Immutable property will not be decoded because it is declared with an initial value which cannot be overwritten." with a fix-it: "Fix: Make the property mutable instead", which fix-it if you use ... would actually break the thing... no idea how to silence it.

On the second thought there's no ambiguity in the call of the form T(), whether the corresponding init is init() or init(a: Int = 0), etc, so in theory decoder could have called it.... (it doesn't).