27

My iOS app has a pretty common setup: it makes HTTP queries to an API server that responds with JSON objects. These JSON objects are then parsed to appropriate Swift objects.

Initially I divided properties into required properties and optional properties, mostly based on my API server's database requirements. For example, id, email, and name are require fields so they use non-optional types. Others can be NULL in database, so they are optional types.

class User {
  let id: Int
  let email: String
  let profile: String?
  let name: String
  let motive: String?
  let address: String?
  let profilePhotoUrl: String?
}

Recently, I started wondering whether this was a good setup at all. I found out that although some properties might be always in the database, that does not mean that those properties will always be included in the JSON response.

For example, in the User profile page, all these fields are needed to properly display the view. Therefore, JSON response will include all these fields. For a view that lists users' names, however, I would not need email or id, and JSON response should probably not include those properties either. Unfortunately, this will cause error and crash the app when parsing JSON response into Swift object since the app expects id, email, name to be always not-nil.

I'm thinking of changing all properties of Swift objects into optionals, but it feels like throwing away all the benefits of this language-specific feature. Moreover, I will have to write many more lines of code to unwrap all these optionals somewhere else in the app anyway.

On the other hand, JSON objects are by their nature not very interoperable with strict static typing and nil-checking of Swift so it might be better to simply accept that annoyance.

Should I transition to models with every property as optionals? Or is there a better way? I'd appreciate any comment here.

mfaani
  • 33,269
  • 19
  • 164
  • 293
Harfangk
  • 814
  • 9
  • 18
  • Reading your `For example, in the User profile page ...` paragraph, it looks like you're parsing JSON in different places/views? Am I reading that right? If yes, then it's part of your problem. – Eric Aya Apr 01 '16 at 11:26
  • @EricD Well, I'm parsing JSON in completion handler for HTTP get requests by using JSON dictionary to initialize Swift objects, so there's only one parsing method. But depending on what API was called, fields in JSON dictionary for same object will vary a lot, sometimes causing parsing issues due to being nil. – Harfangk Apr 01 '16 at 13:09
  • 5
    You should always init your User object completely from the JSON. Then, depending of the destination, you use this object or a derived one. // Like, if you don't want to expose all properties to a view, you could create a DisplayableUser object from the User one, only taking the properties you need. Well, it's just an example, you get the idea. – Eric Aya Apr 01 '16 at 13:12
  • @Harfangk did you get an answer to your question? I don't think the above comments answer the concerns in your question. – Prabhu May 02 '18 at 02:06
  • As you said unwrapping can consume more your time. The simplest way is to initialise the required non optional variables with default values. eg: var name : String = "" – ARSHWIN DENUEV LAL May 07 '18 at 09:09
  • @Prabhu you might want to see [here](https://stackoverflow.com/questions/44288488/when-should-i-use-optionals-and-when-should-i-use-non-optionals-with-default-val). Also make sure you see the comments on the question and accepted answer. – mfaani May 08 '18 at 16:30

11 Answers11

21

There are three ways you can go with this:

  1. Always send all the JSON data, and leave your properties non-optional.

  2. Make all the properties optional.

  3. Make all the properties non-optional, and write your own init(from:) method to assign default values to missing values, as described in this answer.

All of these should work; which one is "best" is opinion-based, and thus out of the scope of a Stack Overflow answer. Choose whichever one is most convenient for your particular need.

Charles Srstka
  • 16,665
  • 3
  • 34
  • 60
  • 1
    Well it was surprising to see this question finally getting answers two years after I posted it. I chose this answer because it is the only one that suggests changing the shape of the data sent by server - which is how I would do it nowadays. I believe that getting always reliable data results in more robust code and that its benefit outweighs the additional network cost incurred by extra bytes. – Harfangk Oct 31 '18 at 09:44
9

The first thing to do is ask: Does an element of the “view that lists users' names” need to be the same kind of object as the model object behind a “User profile page”? Perhaps not. Maybe you should create a model specifically for the user list:

struct UserList: Decodable {

    struct Item: Decodable {
        var id: Int
        var name: String
    }

    var items: [Item]

}

(Although the question said the JSON response might not include id, it doesn't seem like a user list without ids with be particularly useful, so I made it required here.)

If you really want them to be the same kind of object, then maybe you want to model a user as having core properties that the server always sends, and a “details” field that might be nil:

class User: Decodable {
    let id: Int
    let name: String
    let details: Details?

    struct Details: Decodable {
        var email: String
        var profile: String?
        var motive: String?
        var address: String?
        var profilePhotoUrl: String?
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(Int.self, forKey: .id)
        name = try container.decode(String.self, forKey: .name)
        details = container.contains(.email) ? try Details(from: decoder) : nil
    }

    enum CodingKeys: String, CodingKey {
        case id
        case name

        case email // Used to detect presence of Details
    }
}

Note that I create the Details, if it's present, using Details(from: decoder), instead of the usual container.decode(Details.self, forKey: .details). I do it using Details(from: decoder) so that the properties of the Details come out of the same JSON object as the properties of the User, instead of requiring a nested object.

rob mayoff
  • 375,296
  • 67
  • 796
  • 848
5

The premise:

Partial representing is a common pattern in REST. Does that mean all properties in Swift need to be optionals? For example, the client might just need a list of user ids for a view. Does that mean that all the other properties (name, email, etc) need to be marked as optional? Is this good practice in Swift?

Marking properties optional in a model only indicates that the key may or may not come. It allows the reader to know certain things about the model in the first look itself.
If you maintain only one common model for different API response structures and make all the properties optional, whether that's good practice or not is very debatable.
I have done this and it bites. Sometimes it's fine, sometimes it's just not clear enough.

Keeping one model for multiple APIs is like designing one ViewController with many UI elements and depending on particular cases, determining what UI element should be shown or not.
This increases the learning curve for new developers as there's more understanding-the-system involved.


My 2 cents on this:

Assuming we are going ahead with Swift's Codable for encoding/decoding models, I would break it up into separate models rather than maintaining a common model with all optionals &/or default values.

Reasons for my decision are:

  1. Clarity of Separation

    • Each model for a specific purpose
    • Scope of cleaner custom decoders
      • Useful when the json structure needs a little pre-processing
  2. Consideration of API specific additional keys that might come later on.

    • What if this User list API is the only one requiring more keys like, say, number of friends or some other statistic?
      • Should I continue to load a single model to support different cases with additional keys that come in only one API response but not another?
      • What if a 3rd API is designed to get user information but this time with a slightly different purpose? Should I over-load the same model with yet more keys?
    • With a single model, as the project continues to progress, things could get messy as key availability in now very API-case-based. With all being optionals we will have alot of optional bindings & maybe some shortcut nil coalescings here and there which we could have avoided with dedicated models in the first place.
  3. Writing up a model is cheap but maintaining cases is not.

However, if I was lazy and I have a strong feeling crazy changes aren't coming up ahead, I would just go ahead making all the keys optionals and bear the associated costs.

staticVoidMan
  • 19,275
  • 6
  • 69
  • 98
4

I typically make all non-critical properties optional, and then have a failable initializer. This allows me to better handle any changes in the JSON format or broken API contracts.

For example:

class User {
  let id: Int
  let email: String
  var profile: String?
  var name: String?
  var motive: String?
  var address: String?
  var profilePhotoUrl: String?
}

This means that I will never have a user object without an id or email (let's assume those are the two that always need to be associated with a user). If I get a JSON payload without an id or email, the Initializer in the User class will fail and won't create the user object. I then have error handling for failed initializers.

I'd much rather have a swift class with optional properties than a bunch of properties with an empty string value.

r3c0d3
  • 296
  • 3
  • 11
4

I recommend to keep all non-scalar(String, Custom Types etc) properties as optional, scalar(Int, Float, Double etc) as non-optional(with some exceptions) by assigning a default value and collections with empty array. e.g,

class User {
    var id: Int = 0
    var name: String?
    var friends: [User] = []
    var settings: UserSettings?
}

This assures you a crash free app no matter what happens to server. I would prefer abnormal behavior over a crash.

Kamran
  • 14,987
  • 4
  • 33
  • 51
4

This really depends on the way you are handling your data. If you are handling your data through a "Codable" class, then you have to write a custom decoder to throw an exception when you don't get certain expected values. Like so:

 class User: Codable {
    let id: Int
    let email: String
    let profile: String?
    let name: String
    let motive: String?
    let address: String?
    let profilePhotoUrl: String?

     //other methods (such as init, encoder, and decoder) need to be added below.
    }

Because I know that I'm going to need to return an error if I don't get the minimum required parameters, you would need something like an Error enum:

    enum UserCodableError: Error {
         case missingNeededParameters
         //and so on with more cases
    }

You should be using coding keys to keep things consistent from the server. A way to do that inside of the User Object would be like so:

    fileprivate enum CodingKeys: String, CodingKey {
       case id = "YOUR JSON SERVER KEYS GO HERE"
       case email
       case profile
       case name
       case motive
       case address
       case profilePhotoUrl
    }

Then, you need to write your Decoder. A way to do that would be like so:

    required init(from decoder: Decoder) throws {
    let values = try decoder.container(keyedBy: CodingKeys.self)
    guard let id = try? values.decode(Int.self, forKey: .id), let email = try? values.decode(String.self, forKey: .email), let name = try? values.decode(String.self, forKey: .name) else {
        throw UserCodableError.missingNeededParameters
    }

    self.id = id
    self.email = email
    self.name = name

    //now simply try to decode the optionals
    self.profile = try? values.decode(String.self, forKey: .profile)
    self.motive = try? values.decode(String.self, forKey: .motive)
    self.address = try? values.decode(String.self, forKey: .address)
    self.profilePhotoUrl = try? values.decode(String.self, forKey: .profilePhotoUrl)
}

SOMETHING TO NOTE: You should write your own encoder as well to stay consistent.

All of that can go, simply to a nice calling statement like this:

    if let user = try? JSONDecoder().decode(User.self, from: jsonData) {
        //do stuff with user
    }

This is probably the safest, swift-ist, and most object oriented way to handle this type of issue.

MegaTyX
  • 116
  • 5
3

If the server is giving Null value for the other properties, you can go for optionals and safe unwrap. Or while unwrapping you can assign empty string to property if the value is nil

profile = jsonValue ?? ""

Other case since the other properties are String data type you can assign default value as a empty string

class User {
  let id: Int
  let email: String
  let profile: String = ""
  let name: String
  let motive: String = ""
  let address: String = ""
  let profilePhotoUrl: String = ""
}
Prateek kumar
  • 244
  • 3
  • 9
  • Not as of Swift 4.1.2. Unfortunately, JSONDecoder.decode() does not utilize the default values, and you must handle for non-optionals in Decodable.init(from: decoder) – Collierton Aug 11 '18 at 19:46
  • @JeffCollier you can make the properties as optional for codable protocol struct User: Codable { let id: Int? let email: String? let profile: String? let name: String? let motive: String? let address: String? let profilePhotoUrl: String? } – Prateek kumar Aug 29 '18 at 06:23
  • the original poster requested to avoid optionals. Further, I was correcting your example with the default values. While that should work, Swift has not yet incorporated those default values into the decoding. Thus, the need to use .init() – Collierton Oct 02 '18 at 22:25
3

Yes, you should use optional if the property is not necessary in API and if you want some value in the mandatory property then assign blank value:

class User {
  let id: Int?
  let email: String? = ""
  let profile: String?
  let name: String? = ""
  let motive: String?
  let address: String?
  let profilePhotoUrl: String?
}
Jogendar Choudhary
  • 3,476
  • 1
  • 12
  • 26
3

In my opinion, I will choose 1 of 2 solutions:

  1. Edit my init func from JSON to object, init with default object values for all props (id = -1, email = ''), then read JSON with optional checking.
  2. Create a new class/struct for that specific case.
tuledev
  • 10,177
  • 4
  • 29
  • 49
0

I would prefer using failable Initializer its neat compared to other options.

So keep the required properties as non-optionals and create object only if they are present in the response (you can use if-let or gaurd-let to check this in response), else fail the creation of the object.

Using this approach we avoid making non-optionals as optionals and having a pain to handle them throughout the program.

Also optionals are not meant for defensive programming so don't abuse optionals by making "non-optional" properties as optionals.

Sanju
  • 31
  • 1
  • 4
-1

I would prefer optional properties because you can not promise JSON values to be there all the time and any change on response property name would crash your app.

If you do not use optional values, you have to control parameters while parsing and add a default value if you want a crash free app. And you wouldn't know if it was nil or empty string from server.

Optional values is your best friends.

object mapper for mutable and non-mutable properties.

realm-swift for default non-optional values.

ymutlu
  • 6,585
  • 4
  • 35
  • 47