Blog

Encoding Custom Types with Encodable In Swift 4

Photo by  Steve Johnson  on  Unsplash

This post is the third of a ten-part series called Exploring Codable and Core Data in Swift 4.


Encoding Custom Types Automatically

To be consistent with our previous posts in the series, we’re going to continue to use The Movie Database API for this series, because it will provide the best real-world example for using Codable in your own application(s). For instance, let’s start with a JSON object representing a favorited item in a user account’s favorites list—say, Favorite:

Action: Genre
    {
   "media_type": "movie",
   "media_id": 500,
   "favorite": true
}

An account favorite model consists of a media_type, media_id, and favorite. Here’s an example of this model in Swift:

Action: Genre
    struct FavoritedMedia {
   let id: Int
   let type: String
   let isFavorited: Bool
}

As we said in the previous post, because Int, String and Bool already conform to Codable, by having FavoritedMedia conform to Encodable, we get an automatic conformance that satisfies all requirements of Encodable:

Action: Genre
    struct FavoritedMedia: Encodable {
   let id: Int
   let type: String
   let isFavorited: Bool
   // The Encodable method encode(to:) is implemented by default.
}

Encoding Custom Types Manually

In the example below, the Movie structure is extended to conform to the Decodable protocol by implementing its required function, encode(to:):

Action: Genre
    extension FavoritedMedia: Encodable {
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(id, forKey: .id)
        try container.encode(type, forKey: .type)
        try container.encode(isFavorited, forKey: .isFavorited)
    }
}

Now that we’ve gone over how to encode these values 1:1, let’s see how we can use the power of the Swift enum to make our logic a bit more safe. Next, we’ll convert our type property into a nested enum called FavoritedMedia.MediaType:

Action: Genre
    struct FavoritedMedia {
    let id: Int
    let type: MediaType
    let isFavorited: Bool
}

extension FavoritedMedia {
    enum MediaType: String {
        case movie
        case tv
    }
}

extension FavoritedMedia: Encodable {
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(id, forKey: .id)
        try container.encode(type.rawValue, forKey: .type)
        try container.encode(isFavorited, forKey: .isFavorited)
    }
}

Decoding Custom Types with Decodable in Swift 4

Photo by  Steve Johnson  on  Unsplash

This post is the second of a ten-part series called Exploring Codable and Core Data in Swift 4.


Conforming to Codable

There are two ways of implementing Codable on your own types—automatic and manual declaration. Both of which are essential in understanding to proper serialize data to and/or from any Swift type. First, we’ll go over the fundamentals for Codable conformance:

  • Many standard library types conform to Codable by default, such as StringInt, and Double

  • A few Foundation types also conform, such as DateData, and URL

  • Collection and mappable types, such as Array,  Dictionary, and Optional automatically gain conformance to Codable when their contents also conform to Codable

  • When a custom type consists of properties which all conform to Codable, then the custom type automatically conforms as well with proper declaration

  • Custom Codable types may be defined as either structs or classes


Decoding Custom Types Automatically

Next, we’re going to use The Movie Database API for this series, because it will provide the best real-world example for using Codable in your own application(s). For instance, let’s start with a JSON object representing a movie genre—say, Action:

Action: Genre
    {
   "id": 28,
   "name": "Action"
}

A genre model consists of an id and name. Here’s an example of this model in Swift:

Action: Genre
    struct Genre {
    let id: Int
    let name: String
}

As we said above, because Int and String already conform to Codable, by having Genre also conform, we get an automatic conformance that satisfies all requirements of Encodable and Decodable:

Action: Genre
    struct Genre: Codable {
    let id: Int
    let name: String
    // The Codable methods init(from:) and encode(to:) are implemented by default.
}

The next example is for a larger JSON object representing the movie, Aquaman. Let’s assume this movie object, for the time being, is a simplified data model—only representing the id, title, tagline, and genres.

Action: Genre
    struct Movie: Decodable {
    // Int and String both conform to Codable.
    let id: Int
    let title: String
    let tagline: String
    // The Decodable method init(from:) is implemented by default.

    // Movie is still decodable after adding an Array of Genre, which is also decodable.
    let genres: [Genre]
}

Coding Keys

In order to support manual decoding or encoding, Codable needs to know which data attributes should be transformed into Codable types, such as Int, Float, Double, Bool, String, etc.

Codable types can declare a nested enumeration called CodingKeys which conforms to the CodingKey protocol. When this enumeration is declared, the cases listed represent the properties which should be encoded or decoded from the external data to your codable type.

Properties can be ignored from encoding or decoding by omitting their key from the enumeration declaration.

Note: When omitting properties, a default value must be provided in the initializer to support automatic Codable conformance.

Because most languages have their own naming conventions, we can't assume that a given key from JSON should be the exact property name in its Swift representation. Thankfully, CodingKey also supports RawRepresentable which allows our properties to follow the Swift API Design Guidelines, while the raw value can represent the JSON key.

The example below uses alternative keys for our Movie model.

Action: Genre
    struct Movie: Decodable {
    // Int, String, Date, and Double all conform to Codable.
    let id: Int
    let title: String
    let tagline: String
    let releaseDate: Date
    let voteAverage: Double
    let voteCount: Int

    // Some JSON keys should be renamed to follow the API Design Guidelines.
    enum CodingKeys: String, CodingKey {
        case id
        case title       = "original_title"
        case tagline
        case releaseDate = "release_date"
        case voteAverage = "vote_average"
        case voteCount   = "vote_count"
    }
}

Decoding Custom Types Manually

Even though codable types handle both encoding and decoding, we should be explicit about the functionality we would like to expose when implementing the protocol(s) on our own types. Therefore, if our custom type will only be used to decode values, we should conform to Decodable instead of Codable; we should conform to Encodable when we only need to encode values as well.


In the example below, the Movie structure is extended to conform to the Decodable protocol by implementing its required initializer, init(from:):

Action: Genre
    extension Movie: Decodable {
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        id = try values.decode(Int.self, forKey: .id)
        title = try values.decode(String.self, forKey: .title)
        tagline = try values.decode(String.self, forKey: .tagline)

        // We decode the value to String, then transform into the desired Date type.
        let releaseDateString = try values.decode(String.self, forKey: .releaseDate)
        releaseDate = DateFormatter.date(from: releaseDateString)

        voteAverage = try values.decode(Double.self, forKey: .voteAverage)
        voteCount = try values.decode(Int.self, forKey: .voteCount)
    }
}

Note: For more information about the container types used when customizing the encoding and decoding process, see KeyedDecodingContainerProtocol and UnkeyedDecodingContainer.