Search code examples
jsonswift

Swift JSONDecoder replace all missing keys by default value?


I want to set up a remote configuration file for staged feature releases, seasonal changes, offers etc. that wouldn't be worth going through App Review for.

I used to use a \n separated text file but for multi-line strings this gets a bit awkward real quick.

Instead of importing some bloated framework like Firebase for this, I am currently writing a small singleton that parses a configuration file from a remote URL.

I am however facing one issue now:

If the remote json contains a key that's not defined in my Codable struct everything works fine, I still get my object with all the defined keys. Not so much the other way around, if the json is missing a key defined in the struct, JSONDecoder is not able to decode. Example:

    let testJSON = """
{"version":1,"includedB":"B","___notIncludedC":"C"}
"""

struct DefaultConfiguration : Codable {
    var version = 1
    var includedB = "2"
    var notIncludedC = "3"
}

I can make the decoding """work""" by defining the notIncludedC to be of an optional String? - this however makes the result be nil after decoding instead of keeping its predefined default value.

All the answers on SO mention defining custom methods for each key, but I would rather like to use a "skip unknown and keep the value" approach because for big JSON's that would come with a lot of overhead code.


Solution

  • As it was described in the comments you will have to write your own init() since the synthesized one can not provide the behavior you need:

    let testJSON = """
    {"version":1,"includedB":"B","notIncludedC":"C"}
    """
    
    struct DefaultConfiguration : Codable {
        var version = 1
        var includedB = "2"
        var notIncludedC = "3"
    
        enum CodingKeys: String, CodingKey {
            case version
            case includedB
            case notIncludedC
         }
    
        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            version = try container.decode(Int.self, forKey: .version)
            includedB = try container.decode(String.self, forKey: .includedB)
            notIncludedC = try container.decodeIfPresent(String.self, forKey: .notIncludedC) ?? "3"
        }
    }