175

Swift 4 added the new Codable protocol. When I use JSONDecoder it seems to require all the non-optional properties of my Codable class to have keys in the JSON or it throws an error.

Making every property of my class optional seems like an unnecessary hassle since what I really want is to use the value in the json or a default value. (I don't want the property to be nil.)

Is there a way to do this?

class MyCodable: Codable {
    var name: String = "Default Appleseed"
}

func load(input: String) {
    do {
        if let data = input.data(using: .utf8) {
            let result = try JSONDecoder().decode(MyCodable.self, from: data)
            print("name: \(result.name)")
        }
    } catch  {
        print("error: \(error)")
        // `Error message: "Key not found when expecting non-optional type
        // String for coding key \"name\""`
    }
}

let goodInput = "{\"name\": \"Jonny Appleseed\" }"
let badInput = "{}"
load(input: goodInput) // works, `name` is Jonny Applessed
load(input: badInput) // breaks, `name` required since property is non-optional
Kqtr
  • 5,824
  • 3
  • 25
  • 32
zekel
  • 9,227
  • 10
  • 65
  • 96
  • One more query what can i do if I have multiple keys in my json and i want to write a generic method to map json to create object instead of giving nil it should give default value atleast. – Aditya Sharma Mar 08 '18 at 16:43

8 Answers8

185

You can implement the init(from decoder: Decoder) method in your type instead of using the default implementation:

class MyCodable: Codable {
    var name: String = "Default Appleseed"

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        if let name = try container.decodeIfPresent(String.self, forKey: .name) {
            self.name = name
        }
    }
}

You can also make name a constant property (if you want to):

class MyCodable: Codable {
    let name: String

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        if let name = try container.decodeIfPresent(String.self, forKey: .name) {
            self.name = name
        } else {
            self.name = "Default Appleseed"
        }
    }
}

or

required init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.name = try container.decodeIfPresent(String.self, forKey: .name) ?? "Default Appleseed"
}

Re your comment: With a custom extension

extension KeyedDecodingContainer {
    func decodeWrapper<T>(key: K, defaultValue: T) throws -> T
        where T : Decodable {
        return try decodeIfPresent(T.self, forKey: key) ?? defaultValue
    }
}

you could implement the init method as

required init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.name = try container.decodeWrapper(key: .name, defaultValue: "Default Appleseed")
}

but that is not much shorter than

    self.name = try container.decodeIfPresent(String.self, forKey: .name) ?? "Default Appleseed"
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • Also note that in this particular case, you can use the auto-generated `CodingKeys` enumeration (so can remove the custom definition) :) – Hamish Jun 15 '17 at 19:44
  • @Hamish:It did not compile when I first tried it, but now it works :) – Martin R Jun 15 '17 at 19:45
  • Yeah, it's currently a bit patchy, but will be fixed (https://bugs.swift.org/browse/SR-5215) – Hamish Jun 15 '17 at 19:46
  • Great answer. Thanks. Followup question: I have many numeric properties. Is there a good way to wrap the `decodeIfPresent` call? Ideally I could use it like this: `self.myproperty = decodeWrapper(key: .myproperty, defaultValue: 0)` How can I write a function (inside init?) that takes those generated CodingKeys? – zekel Jun 15 '17 at 20:50
  • 84
    It's still ridiculous that auto-generated methods cannot read the default values from non-optionals. I have 8 optionals and 1 non-optional, so now writing manually both Encoder and Decoder methods would bring a lot of boilerplate. `ObjectMapper` handles this very nicely. – Legoless Aug 30 '17 at 21:49
  • Can you elaborate on ObjectMapper usage? I'm not familiar with that. – Mark A. Donohoe Jan 19 '18 at 23:17
  • One more query what can i do if I have multiple keys in my json and i want to write a generic method to map json to create object instead of giving nil it should give default value atleast. – Aditya Sharma Mar 08 '18 at 16:42
  • Structs, anyone? – Jonny Nov 09 '18 at 08:14
  • 2
    This really annoying when we use `codable` but still must custom for missing key in json :( – lee Nov 13 '18 at 12:06
  • Hm. I wonder if there would be some clever way of catching decoding errors, then using the error definition to check which key was missing. Create an extension of Codable that returns defaultValue for key, inserts this value into the original json being parsed, and retries. – Nailer Nov 15 '18 at 16:54
  • @Hamish When I remove my CodingKeys declaration and try using auto-generated CodingKeys I get **Use of unresolved identifier 'CodingKeys'; did you mean 'CodingKey'?** I got it I need to change Decodable to Codable – Leo Dabus Mar 15 '19 at 13:37
  • 1
    @LeoDabus Could it be that you're conforming to `Decodable` and are also providing your own implementation of `init(from:)`? In that case the compiler assumes you want to handle decoding manually yourself and therefore doesn't synthesise a `CodingKeys` enum for you. As you say, conforming to `Codable` instead works because now the compiler is synthesising `encode(to:)` for you and so also synthesises `CodingKeys`. If you also provide your own implementation of `encode(to:)`, `CodingKeys` will no longer be synthesised. – Hamish Mar 15 '19 at 13:44
  • I did figured it out Thanks https://stackoverflow.com/questions/55131400/swift-decode-imprecise-decimal-correctly/55131900#55131900 – Leo Dabus Mar 15 '19 at 13:44
  • For small json files with primitive types yes may be a simple and clean solution.. But how to handle members of type - non-primitive classes ? It will add lot of boiler plate code while I am decoding humongous json files with bunch of nested classes in each ? Here default objects to be created isn't it - which will be a mess ? – Srivathsa Feb 16 '23 at 06:27
67

You can use a computed property that defaults to the desired value if the JSON key is not found.

class MyCodable: Decodable {
    var name: String { return _name ?? "Default Appleseed" }
    var age: Int?

    // this is the property that gets actually decoded/encoded
    private var _name: String?

    enum CodingKeys: String, CodingKey {
        case _name = "name"
        case age
    }
}

If you want to have the property read-write, you can also implement the setter:

var name: String {
    get { _name ?? "Default Appleseed" }
    set { _name = newValue }
}

This adds a little extra verbosity as you'll need to declare another property, and will require adding the CodingKeys enum (if not already there). The advantage is that you don't need to write custom decoding/encoding code, which can become tedious at some point.

Note that this solution only works if the value for the JSON key either holds a string or is not present. If the JSON might have the value under another form (e.g. it's an int), then you can try this solution.

Cristik
  • 30,989
  • 25
  • 91
  • 127
  • Interesting approach. It does add a bit of code but it's very clear and inspectable after the object is created. – zekel Mar 03 '19 at 22:45
  • 3
    My favorite answer to this issue. It allows me to still use the default JSONDecoder and easily make an exception for one variable. Thanks. – iOS_Mouse Jan 07 '20 at 17:05
  • 1
    Note: Using this approach your property becomes get-only, you cannot assign value directly to this property. – Ganpat Jul 28 '20 at 11:26
  • @Ganpat good point, I updated the answer to also provide support for readwrite properties. Thanks, – Cristik Oct 22 '20 at 11:12
  • This is my favorite answer too. The only thing that would be better is if you didn't have to duplicate the CodingKeys that don't need special treatment, but could list just those that were deviant – Travis Griggs Sep 14 '22 at 17:02
  • 1
    Unfortunately that doesn't generate anything when using `JSONEncoder.encode`, like it leaves us with a string of `{}` – user2875404 Dec 01 '22 at 23:09
  • @user2875404 this works mainly for `Decodable` (updated the answer) since computed properties don't implicitly participate in the encoding process. – Cristik Jan 05 '23 at 12:45
33

Approach that I prefer is using so called DTOs - data transfer object. It is a struct, that conforms to Codable and represents the desired object.

struct MyClassDTO: Codable {
    let items: [String]?
    let otherVar: Int?
}

Then you simply init the object that you want to use in the app with that DTO.

 class MyClass {
    let items: [String]
    var otherVar = 3
    init(_ dto: MyClassDTO) {
        items = dto.items ?? [String]()
        otherVar = dto.otherVar ?? 3
    }

    var dto: MyClassDTO {
        return MyClassDTO(items: items, otherVar: otherVar)
    }
}

This approach is also good since you can rename and change final object however you wish to. It is clear and requires less code than manual decoding. Moreover, with this approach you can separate networking layer from other app.

Leonid Silver
  • 384
  • 4
  • 8
  • Some of the other approaches worked fine but ultimately I think something along these lines is the best approach. – zekel Aug 04 '19 at 17:08
  • good to known, but there is too much code duplication. I prefer Martin R answer – Kamen Dobrev May 18 '20 at 07:41
  • 1
    There would be no code duplication if you use services like https://app.quicktype.io to generate DTO from your JSON. There will be even less typing, actually – Leonid Silver Jul 21 '21 at 15:26
  • I love quicktype.io and use it server side to generate C++ code and your comment set my light bulb on that I already have the example JSON i need to generate my Swift client model code and wow I'm inspired can you tell? :-) – moodboom Jul 12 '22 at 02:47
  • @LeonidSilver how would that work? I'm not seeing any options for that in quicktype.io or being able to add a default value? – user2161301 Dec 23 '22 at 21:19
31

You can implement.

struct Source : Codable {

    let id : String?
    let name : String?

    enum CodingKeys: String, CodingKey {
        case id = "id"
        case name = "name"
    }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        id = try values.decodeIfPresent(String.self, forKey: .id) ?? ""
        name = try values.decodeIfPresent(String.self, forKey: .name)
    }
}
Suraj Rao
  • 29,388
  • 11
  • 94
  • 103
  • 4
    yes this is the cleanest answer, but it still gets a lot of code when you have big objects! – Ashkan Ghodrat Aug 05 '20 at 08:04
  • This is more concise and easier to read! This worked for my JSON data with nil values. I think this really should be the accepted answer. – stromyc Feb 02 '23 at 17:10
12

I came across this question looking for the exact same thing. The answers I found were not very satisfying even though I was afraid that the solutions here would be the only option.

In my case, creating a custom decoder would require a ton of boilerplate that would be hard to maintain so I kept searching for other answers.

I ran into this article that shows an interesting way to overcome this in simple cases using a @propertyWrapper. The most important thing for me, was that it was reusable and required minimal refactoring of existing code.

The article assumes a case where you'd want a missing boolean property to default to false without failing but also shows other different variants. You can read it in more detail but I'll show what I did for my use case.

In my case, I had an array that I wanted to be initialized as empty if the key was missing.

So, I declared the following @propertyWrapper and additional extensions:

@propertyWrapper
struct DefaultEmptyArray<T:Codable> {
    var wrappedValue: [T] = []
}

//codable extension to encode/decode the wrapped value
extension DefaultEmptyArray: Codable {
    
    func encode(to encoder: Encoder) throws {
        try wrappedValue.encode(to: encoder)
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        wrappedValue = try container.decode([T].self)
    }
    
}

extension KeyedDecodingContainer {
    func decode<T:Decodable>(_ type: DefaultEmptyArray<T>.Type,
                forKey key: Key) throws -> DefaultEmptyArray<T> {
        try decodeIfPresent(type, forKey: key) ?? .init()
    }
}

The advantage of this method is that you can easily overcome the issue in existing code by simply adding the @propertyWrapper to the property. In my case:

@DefaultEmptyArray var items: [String] = []

Hope this helps someone dealing with the same issue.


UPDATE:

After posting this answer while continuing to look into the matter I found this other article but most importantly the respective library that contains some common easy to use @propertyWrappers for these kind of cases:

https://github.com/marksands/BetterCodable

lbarbosa
  • 2,042
  • 20
  • 23
  • So does this help at all using Firestore Codable when fields no longer exist in an object? – justdan0227 Aug 06 '21 at 13:23
  • 1
    Yes, you can create a property wrapper that defaults to a certain value based on the type if the key is missing from the object. – lbarbosa Aug 06 '21 at 14:03
  • Almost perfect. Unfortunately only leaves me with an empty array even if I add values to it in the declaration. it seems to omit the value that is initially set and defaults to the `= []` – user2875404 Dec 01 '22 at 23:36
3

If you don't want to implement your encoding and decoding methods, there is somewhat dirty solution around default values.

You can declare your new field as implicitly unwrapped optional and check if it's nil after decoding and set a default value.

I tested this only with PropertyListEncoder, but I think JSONDecoder works the same way.

Kirill Kuzyk
  • 99
  • 1
  • 7
1

If you think that writing your own version of init(from decoder: Decoder) is overwhelming, I would advice you to implement a method which will check the input before sending it to decoder. That way you'll have a place where you can check for fields absence and set your own default values.

For example:

final class CodableModel: Codable
{
    static func customDecode(_ obj: [String: Any]) -> CodableModel?
    {
        var validatedDict = obj
        let someField = validatedDict[CodingKeys.someField.stringValue] ?? false
        validatedDict[CodingKeys.someField.stringValue] = someField

        guard
            let data = try? JSONSerialization.data(withJSONObject: validatedDict, options: .prettyPrinted),
            let model = try? CodableModel.decoder.decode(CodableModel.self, from: data) else {
                return nil
        }

        return model
    }

    //your coding keys, properties, etc.
}

And in order to init an object from json, instead of:

do {
    let data = try JSONSerialization.data(withJSONObject: json, options: .prettyPrinted)
    let model = try CodableModel.decoder.decode(CodableModel.self, from: data)                        
} catch {
    assertionFailure(error.localizedDescription)
}

Init will look like this:

if let vuvVideoFile = PublicVideoFile.customDecode($0) {
    videos.append(vuvVideoFile)
}

In this particular situation I prefer to deal with optionals but if you have a different opinion, you can make your customDecode(:) method throwable

Eugene Alexeev
  • 1,152
  • 12
  • 32
0

I wanted the default value if a key is not present in the API response, so I implemented the following solution. Hope it helps.

class MyModel: Codable {
    
    let id: String? = "1122"
    let name: String? = "Syed Faizan Ahmed"
    
    enum CodingKeys: String, CodingKey {
        case id = "id"
        case name = "name"
    }
    
    required init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        id = try values.decodeIfPresent(String.self, forKey: .id) ?? id
        name = try values.decodeIfPresent(String.self, forKey: .name) ?? name
    }
}
  • The goal of the question is to avoid having optional properties. There's no need to make your properties optional. There's also no need to make them `var`. Put the default value after the `??` instead of putting the default in the property initializer. But then at that point you have the same answer as "Martin R". – HangarRash Mar 26 '23 at 14:51
  • Thanks for highlighting this, I have edited my answer and changed the var to let. I have made the properties optional with default values so that we could check on the values coming into the model. I have put the default in the property for better visibility of a model class, we can see clearly what are the default values of the models. "Martin R" answer is different, for that you have to write an extra functionality of decodeWrapper. – Syed Faizan Ahmed Apr 06 '23 at 05:31
  • *"I have made the properties optional"* - but as I said, the goal is to avoid having optional properties. That's the whole point of the original question. Your update doesn't improve it at all. In fact, your update won't even compile now. – HangarRash Apr 06 '23 at 14:30