48

I am using Codable protocol from Swift 4 first time, I am not able to understand use of decodeIfPresent from Decodable.

/// Decodes a value of the given type for the given key, if present.
///
/// This method returns `nil` if the container does not have a value associated with `key`, or if the value is null. The difference between these states can be distinguished with a `contains(_:)` call.
///
/// - parameter type: The type of value to decode.
/// - parameter key: The key that the decoded value is associated with.
/// - returns: A decoded value of the requested type, or `nil` if the `Decoder` does not have an entry associated with the given key, or if the value is a null value.
/// - throws: `DecodingError.typeMismatch` if the encountered encoded value is not convertible to the requested type.
public func decodeIfPresent(_ type: String.Type, forKey key: KeyedDecodingContainer.Key) throws -> String?

Here it suggest that it returns nil, if value not present with associated key. If this is the only reason , then how it differ from optional property, as optional variable also set to nil if value is not present in response.

technerd
  • 14,144
  • 10
  • 61
  • 92
  • 1
    Are you aware that the method returns a `String?`, which _is_ an optional? So basically you are right. There _is_ a use of optionals. – Sweeper Sep 19 '17 at 05:38

3 Answers3

105

There's a subtle, but important difference between these two lines of code:

// Exhibit 1
foo = try container.decode(Int?.self, forKey: .foo)
// Exhibit 2
foo = try container.decodeIfPresent(Int.self, forKey: .foo)

Exhibit 1 will parse:

{
  "foo": null,
  "bar": "something"
}

but not:

{
  "bar": "something"
}

while exhibit 2 will happily parse both. So in normal use cases for JSON parsers you'll want decodeIfPresent for every optional in your model.

Gunter Hager
  • 1,271
  • 1
  • 7
  • 4
  • 3
    found answer: while not mentioned in the docs [here](https://developer.apple.com/documentation/swift/keyeddecodingcontainer/2893445-decodeifpresent). It is mentioned in the [comments of the API](https://github.com/apple/swift/blob/master/stdlib/public/core/Codable.swift.gyb#L525) itself which can also be found in the question :D. It will throw _`DecodingError.typeMismatch` if the encountered encoded value is not convertible to the requested type._ The `decode` can also throw three different errors. See [here](https://github.com/apple/swift/blob/master/stdlib/public/core/Codable.swift.gyb#L474) – mfaani Dec 20 '18 at 16:09
  • can the first response be parsed by `foo = try container.decode(Int.self, forKey: .foo)` ? – Somoy Das Gupta Feb 01 '19 at 10:19
  • No, it can't be parsed with `foo = try container.decode(Int.self, forKey: .foo)`, because `Int` can't hold a `null` value (i.e. `nil`). – Gunter Hager Feb 12 '19 at 08:27
  • 1
    Is there a different between `foo = try container.decodeIfPresent(Int.self, forKey: .foo)` and `foo = try? container.decode(Int.self, forKey: .foo)` ? – PoolHallJunkie Oct 30 '19 at 14:50
  • 2
    Yes, because `foo = try? container.decode(Int.self, forKey: .foo)` will decode all occurrences where `"foo"` contains a value, but can't be converted to `Int`. E.g.: `"foo": "something"` will result in `foo` containing `nil`. You should definitely catch the error instead of just ignoring it with `try?`. – Gunter Hager Oct 31 '19 at 15:07
  • But does an optional type means, other that there could be a value or nil, that the key could be missed from the server JSON response? – Coder Feb 19 '21 at 23:52
  • No, if you use an optional type for decoding like `foo = try container.decode(Int?.self, forKey: .foo)` then the JSON must include the key. It'll throw if the JSON is missing the key. – Gunter Hager Feb 22 '21 at 07:14
11

I think it makes sense to use decodeifPresent rather than an optional property if you want to use a default value for a property that could be missing from the JSON.

For example, let's examine 3 situations:

1. All the keys are present in the JSON:

Let's suppose you must decode this JSON:

{
    "project_names": ["project1", "project2", "project3"],
    "is_pro": true
}

You can use this struct:

struct Program: Codable {
    let projectNames: [String]
    let isPro: Bool
}

and you will get a Program object with a isPro value equal to true. (I suppose your decoder keyDecodingStrategy is .convertFromSnakeCase in the rest of this example)


2. Some keys are missing in the JSON and you're ok to have an optional in Swift:

{
    "project_names": ["project1", "project2", "project3"]
}

You can now use this struct:

struct Program: Codable {
    let projectNames: [String]
    var isPro: Bool?
}

and you will get a Program object with a isPro value equal to nil.

If the JSON looked like this:

{
    "project_names": ["project1", "project2", "project3"],
    "is_pro": true
}

then isPro would be a Bool? with value true. Maybe that's what you want, but probably you would like to have a Bool with a default value of false. That's where decodeIfPresent could be useful.


3. Some keys are missing in the JSON and you want a non-optional property with a default value in Swift:

If your struct looks like this:

struct Program: Codable {
    let projectNames: [String]
    var isPro: Bool = false
}

then you will get a parsing error if the "is_pro" attribute is not present in your JSON. Because Codable expects to find a value to parse a Bool property.

In that situation, a good idea would be to have an initializer with decodeIfPresent, like so:

struct Program: Codable {
    let projectNames: [String]
    let isPro: Bool

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.projectNames = try container.decode([String].self, forKey: .projectNames)
        self.isPro = try container.decodeIfPresent(Bool.self, forKey: .isPro) ?? false
    }
}

This allows you to have the best of both worlds:

  • your struct has a Bool, not a Bool? property
  • you are still able to parse a JSON that does NOT contain the "is_pro" field
  • you can get a default value of false if the field is not present in the JSON.
Frederic Adda
  • 5,905
  • 4
  • 56
  • 71
10

Yes, @Sweeper's comment makes a sense.

I will try to explain it according to my understanding.

public class User : Decodable{

    public var firstName:String
    public var lastName:String
    public var middleName:String?
    public var address:String
    public var contactNumber:String


    public enum UserResponseKeys: String, CodingKey{
        case firstName = "first_name"
        case lastName = "last_name"
        case middleName = "middle_name"
        case address = "address"
        case contactNumber = "contact_number"
    }

    public required init(from decoder: Decoder) throws {

        let container = try decoder.container(keyedBy: UserResponseKeys.self)

        self.firstName = try container.decode(String.self, forKey: .firstName)
        self.lastName = try container.decode(String.self, forKey: .lastName)
        self.middleName = try container.decodeIfPresent(String.self, forKey: .middleName)
        self.address = try container.decode(String.self, forKey: .address)
        self.contactNumber = try container.decode(String.self, forKey: .contactNumber)
    }

}

Above is my User class, in which I marked middleName as optional parameter, because it may possible that JSON response may not provide middleName key-value pair in response, so we can use decodeIfPresent.

self.middleName = try container.decodeIfPresent(String.self, forKey: .middleName)

While for others variables which are mandatory fields so we are sure that no need to use of optional for that. We used only decode for that as that method does not return optional.

public func decode(_ type: String.Type, forKey key: KeyedDecodingContainer.Key) throws -> String

Above decode function returns String while decodeIfPresent returns String?, so we can use optional variable to store that.

So final conclusion is that if you are not sure of service response contract or you may dealing with any third party services where JSON response and parameters may change without your knowledge then you can use decodeIfPresent so it can handle absence of particular parameter in response and set value as nil.

mfaani
  • 33,269
  • 19
  • 164
  • 293
technerd
  • 14,144
  • 10
  • 61
  • 92