In this article, we will go through Type-Safe ways of Model design, Codable and how to decrease boilerplates we all have to write in future.

Importance

Why is type-safety important? Unfortunately Architectures don’t care about type-safety and consider modeling and data mappings as easy/useless tasks, But that doesn’t mean we don’t have to do them! If we follow some easy steps & rules, it will save us in a way we never could have imagined.

Application’s Domain

Every application has it’s own way of representing data which is stored somehow inside the application. Some use JSONs, Dictionaries, classes, structs or etc. The point is, there’s always a mapping between raw Data and how it’s stored in Application. That mapping is based on some domain/context or knowledge about your application and what it does. For simplicity of this article, we’re gonna start with something as small as UserProfile and start making it more complex step by step.

UserProfile

Each user profile has a name, lastName and cellNumber for sure, and probably an email.

We see this modeling everyday and we’re all used to it, but… should we? does email say enough about what it holds? what about cellNumber? are they really just a String ?

make a type if it will have some special behavior in its operations that the base type doesn’t have. by Martin Fowler

We might use cellNumber later on, to make calls, or use email to send e-mails with the application, but are they really valid? What we can infer from this code is that they’re optional String s. We can validate them each time we want to use them (😱) or at least, have validators in initializers to check email and cellNumber 's validity for us, but we haven’t expressed it in type-level. What if someone else wants to work with this code? Do they know these fields are validated? Is putting a comment like “these fields are validated!” sufficient? What if there’s another model in the system like Contact working with cellNumber ? Should we duplicate validation?

The least we can do is to use typealias to let others know, these fields are a little different from String.

Have you noticed the existence of CGFloat , TimeInterval , CLLocationDegrees types? They’re all representing Floating Point numbers, so why define different types? The answer is: to provide context. When we see CLLocationDegrees we know that this field will be something between [-180…180]. it does not make sense to have a CLLocationDegrees with value of 1000, just as it doesn’t make sense to have "hello world" as cellNumber .

When you use typealias and define custom types, you can add more context to them in future without changing the public API (probably!). Just like CGFloat . in early releases of Swift, CGFloat was just another typealias to Double , go check it, it’s a whole data-structure now.

So let’s rewrite our typealias es to data-structures now! UserProfile will become:

Now we have more understanding about what UserProfile.email is and what it holds. This way we even limit ourselves to some possible mistakes that might happen in future.

Some types don’t want to use their full capabilities.

Lets say we haven’t rewrote the UserProfile and wanted to present fullName :

somehow we miss-typed and instead of lastName , we used cellNumber . Compiler wouldn’t know that we made a mistake because in Swift’s eyes, they’re both String s and everything will work just fine.We wouldn’t catch this bug except in runtime (or tests). But using CellNumber as the type, we will be able to catch this error in compile time. 🤩

User

Later on, the application changes and uses OTP as login mechanism and we have a User model alongside our UserProfile .

If we take the previous approach and make types more safe, we’ll get:

If we keep doing it, we will see that our models will get too verbose. for example, CellNumber and Email can be first model citizens, but not UserProfile and UserID . They don’t have a meaning outside of User ’s scope.

So let’s put things where they belong.

We’ll use User.Profile instead of UserProfile and User.ID instead of UserID . See how more meaningful they all are now? 🤓

But, still profile and isRegistered are a bit unsafe here. We know that profile exists when isRegistered is true and doesn’t otherwise, but we don’t see that in type-level. (again, more context that we need to know). So let’s use enumeration and pattern matching to embed such meanings into User .

Now we know for sure, that Profile is available only when User.Status is registered and not otherwise.

When to use enum and when not to, is completely related to the domain and context of our application.

If we had a field isEmailVerified which would imply that email is verified from the server-side, it was NOT a good idea to use enum , because validity of the email does not justify it’s existence.

JSON & Codable

First thing we need to know, is that JSONs are not type-safe. They only support basic data types, Arrays and Dictionaries. You should also checkout Sergio Schechtman Sette’s great article on creating type-safe JSONs in Swift.

JSONs have a lot of context in them, because in a JSON file, email and cellNumber are still String s and again, we need to know how to act with each of these fields. 🙄

A sample JSON for User would be something like:

Let’s fix the CellNumber and Email first. Decoder/Encoder have a method named “ singleValueContainer ” which implies that this part of JSON is data itself, we don’t need to query deeper for extracting data.

At this point, we don’t need to implement Codable methods for Profile , since all subsequent types are Codable , compiler will automatically synthesize the implementations and take care of it for us. It only needs CodingKeys because keys in the JSON are different from the keys in our model. (Or not define them in detail, and use JSONDecoder.KeyDecodingStrategy.convertFromSnakeCase instead. Thanks to Kaunteya Suryawanshi)

But we’re gonna have some complexities about User , because there’s no User.Status in the JSON file. This is where we have to get our hands dirty… 👨🏻‍💻

And now, all our models are safely typed while confirming to Codable . We don’t need to know some context or knowledge to understand what they do, they speak for themselves. 🍺

We can still have isRegistered and profile variables just like before, but we don’t have to memorize that context anymore. By just looking at User 's implementation, we can infer it. 😏

User was just a very basic example of data modeling. This way we’ll omit ourselves of mistakes we might possibly make in the future.

Cons of having optional values

Most of the time, they lead to force-unwrapping. One day (probably near the deadlines!) you’ll get frustrated and stop guard checking user.profile , and use it with force-unwrapping like user.profile!.fullName and everything might work just fine, but that’s just fire under the ashes. Later on, we might change User ’s implementation and probably change it’s context. Using the type-safe version, we’ll get notified of changes with a compile-error immideitly and fix’em accordingly, but with force-unwrapping we’re telling the compiler we know what we’re doing, trust us and compile the code. But the truth is, we just knew what we were doing while writing the code (hopefully 😅), we weren’t aware of future changes that might break the code! Remember Murphy’s law

Something that can happen, will happen