Codable: Parsing JSON in Swift

In this article, we’re going to explore the Codable protocol, and how it can be used to convert to and from an external representation such as JSON.

Continue Reading…

Written by

Iñaki Narciso

Published on

23 Jun 2023


A lot of apps have online features that require a network connection in order to work. For example, you need to be online to see the photos in your Instagram feed, or to load and read the latest news in Reddit.

If you’re creating an app with online functionality, you might need to depend on a backend API, and read or send data in the form of JSON.

In this article, we’re going to explore the Codable protocol, and how it can be used to convert to and from an external representation such as JSON.

The Codable Protocol

The Codable protocol combines the Encodable and Decodable protocols into one single conformance. Which means that any conforming custom types (such as an object of a class, struct, or enum) can be encoded to or decoded from an external representation such as JSON or property list.

Suppose that we’re working on a simple book library app, and we have a JSON data for a book that we need to show into the app.

{
    "title": "A fairy tale",
    "pages": 100,
    "authorName": "John Appleseed",
    "yearPublished": 2000
}

To model this JSON data, we’ll create a struct reflecting the properties from the JSON data, and conforming the model to the Codable protocol.

JSON DataStored Property NameType
“title”: “A fairy tale”titleString
“pages”: 100pagesInt
“authorName”: “John Appleseed”authorNameString
“yearPublished”: 2000yearPublishedInt

The stored properties of the Book struct model consists of each of the fields from the JSON data, and the types of each property depends on its respective value from the JSON data.

struct Book: Codable {
    var title: String
    var pages: Int
    var authorName: String
    var yearPublished: Int
}

The Codable conformance declaration makes the Book struct model as both Encodable and Decodable without having the necessary declarations for both protocols.

⚠️ IMPORTANT: The only requirement of declaring a Codable is that all of its stored properties must also be Codable.

Codable Types

In our example above we get to use two types from the Swift standard library, String and Int, which reflects the type of the values from the JSON data. The good news is that types from the Swift standard library such as String and Int are already Codable.

Codable also includes types from Foundation like Date, Data, and URL.

Stored Property NameTypeIs Type Codable?
titleString✅ YES
pagesInt✅ YES
authorNameString✅ YES
yearPublishedInt✅ YES

Collection and Optional Types

Built-in collection types in Swift such as Array, Dictionary, and Optional also conform to Codable only when they contain codable types.

Assume that we need to show a list of books in a shelf for the library view. We have the following JSON data that represents this as follows:

{
    "books": [
        {
            "title": "A fairy tale",
            "pages": 100,
            "authorName": "John Appleseed",
            "yearPublished": 2000
        },
        {
            "title": "Be you",
            "pages": 200,
            "authorName": "Karl Bobson",
            "yearPublished": 2001
        },
        {
            "title": "Cat's life",
            "pages": 300,
            "authorName": "Larry Cast",
            "yearPublished": 2003
        }
    ]
}

We can model this JSON data in another struct we’ll name as Library, containing a single property named books which should be an array of books.

JSON DataStored Property NameTypeIs Type Codable
“books”: [ … ]booksArray\<Book>✅ YES, the Book struct is declared as Codable from the previous example

Let’s see how we can write this in Swift code:

struct Library: Codable {
    var books: [Book]
}

An app’s data schema can change over the course of its lifetime. Suppose that we’re going to separate the Author data from the Book data, and we’ll be having a new JSON data that reflects this schema:

{
    "title": "A fairy tale",
    "pages": 100,
    "yearPublished": 2000,
    "author": {
        "name": "John Appleseed",
        "website": "https://johnappleseed.com"   
    }
}

The schema changed the reference of an Author which was previously included as authorName to an actual JSON object by itself.

JSON DataStored Property NameTypeIs Type Codable?
“title”: “A fairy tale”titleString✅ YES
“pages”: 100pagesInt✅ YES
“yearPublished”: 2000yearPublishedInt✅ YES
“author”: { … }authorAuthor❌ No, since we haven’t created a model for Author yet, but creating it and making it Codable is pretty easy.

Let’s create the Author model to reflect the schema change in our JSON data:

struct Author: Codable {
    var name: String
    var website: URL
}

struct Book: Codable {
    var title: String
    var pages: Int
    var yearPublished: Int
    var author: Author
}

Making the Author conform to Codable will make the Book class Codable as well since all of its properties are now Codable.

JSON DataStored Property NameTypeIs Type Codable?
“title”: “A fairy tale”titleString✅ YES
“pages”: 100pagesInt✅ YES
“yearPublished”: 2000yearPublishedInt✅ YES
“author”: { … }authorAuthor✅ YES

Decoding JSON

Adopting Codable to our custom types such as Book and Author allows us to serialize them from and to a data format using a built-in Encoder and Decoder.

In this example, we’ll use the JSONDecoder to decode a JSON string into its equivalent model type in Swift.

/// This is the JSON data example from above, we'll
/// declare it as a String
let json = """
{
    "title": "A fairy tale",
    "pages": 100,
    "yearPublished": 2000,
    "author": {
        "name": "John Appleseed",
        "website": "https://johnappleseed.com"   
    }
}
"""

/// Then convert it to Data so we can decode it using
/// JSONDecoder
let data = Data(json.utf8)

/// This is the JSONDecoder instance we'll use to
/// convert the JSON string into the model struct
let decoder = JSONDecoder()
do {
    // and this is how we decode it:
    let book = try decoder.decode(Book.self, from: data)

    print(book.title) // prints "A fairy tale"
    print(book.author.name) // prints "John Appleseed"
} catch {
    print(error.localizedDescription)
}

Encoding JSON

In this example, we’ll use JSONEncoder to encode a codable Swift struct (the book object we decoded from the previous example) back to JSON.

let encoder = JSONEncoder()
do {
    /// We'll use the same book instance that we've
    /// created from decoding from the last example
    let data = try encoder.encode(book)
    let json = String(decoding: data, as: UTF8.self)
    print(json)
} catch {
    print(error.localizedDescription)
}

Choosing Properties to Encode or Decode

In the example above, all of the stored properties are encoded and decoded automatically by conforming the model to the Codable protocol. But what if you don’t need to encode/decode all stored properties of the model object?

The answer is to use Coding Keys. All Codable types can declare a special nested enum called CodingKeys that conforms to the CodingKey protocol.

struct Book: Codable {
    var title: String
    var pages: Int
    var yearPublished: Int
    var author: Author

    /// No need to declare this enum if all properties
    /// are to be included in encoding/decoding
    /// but this is shown as an example
    enum CodingKeys: String, CodingKey {
        case title, pages, yearPublished, author
    }
}

The CodingKeys enum will serve as the list of properties that must be encoded/decoded as part of the Codable model object.

⚠️ IMPORTANT: The names of the enum cases should match the stored property names.

In such cases when you want to encode properties having different names in their representation, you can do so by providing a String raw value to the enum case.

For example, we have the following JSON structure, and we wanted our Codable type to encode/decode to and from such a structure, we can do so by declaring its CodingKey enum case equivalent matching to its name in JSON.

{
    "title": "A fairy tale",
    "number_of_pages": 100,
    "year_published": 2000,
    "author": {
        "name": "John Appleseed",
        "website": "https://johnappleseed.com"   
    }
}
struct Book: Codable {
    var title: String
    var pages: Int
    var yearPublished: Int
    var author: Author

    enum CodingKeys: String, CodingKey {
        /// the name of the stored property
        /// and the name of the JSON key matches
        /// for `title` and `author`
        case title, author

        /// However, `pages` and `yearPublished`
        /// Doesn't have a matching key in the
        /// JSON structure, as such, we need to
        /// declare its key equivalent as a String
        case pages = "number_of_pages"
        case yearPublished = "year_published"
    }
}

Ignoring properties in JSON

If there are stored property names that aren’t part of it, then it is ignored by the encoder/decoder.

⚠️ IMPORTANT: Properties that you don’t declare as part of the CodingKeys enum should be given a default value in order for its containing type to receive automatic conformance to Decodable or Codable.

struct Book: Codable {
    var title: String
    var yearPublished: Int
    var author: Author
    /// Since we're omitting this, we should provide a
    /// default value to allow `Book` to conform to
    /// Codable
    var pages: Int = 100

    enum CodingKeys: String, CodingKey {
        /// In this example, `pages` is omitted
        /// intentionally
        ///
        /// Which means, only these three properties
        /// will be included in the stored properties
        /// to be encoded/decoded
        case title, yearPublished, author
    }
}

Handling nullable JSON Properties

There might be cases when a JSON property returns null. In such cases, we can declare the nullable property as an Optional in our Codable type.

{
    "title": "A fairy tale",
    "pages": null,
    "yearPublished": 2000,
    "author": {
        "name": "John Appleseed",
        "website": "https://johnappleseed.com"   
    }
}
struct Book: Codable {
    var title: String
    /// Might be `null`, so in such case its value
    /// will simply be nil
    var pages: Int?
    var yearPublished: Int
    var author: Author
}

Codable Enums for Groups of Values

If we have a JSON property with a fixed group of values, and we know what these values are, then we can use enums to represent these values and it will add type-safety into our code.

For example, we’ll add a new genre property in the Book struct, and there are four possible values (just an example) that the genre property could have: fiction, business, novel, and comics. We can declare this as a nested enum, and declare each value as a unique enum case.

struct Book: Codable {
    /// The genre enum with four possible values
    enum Genre: String, Codable {
        case fiction, business, novel, comics
    }

    var title: String
    var pages: Int
    var yearPublished: Int

    /// Declare the genre here and it will be
    /// Encoded/Decoded accordingly
    var genre: Genre
}

Now that we have declared a Codable enum to represent all possible values for the new genre property, we can now use the Book struct to encode to or decode from JSON.

let json = """
{
    "title": "A fairy tale",
    "pages": 100,
    "yearPublished": 2000,
    "genre": "comics"
}
"""

let data = Data(json.utf8)
let decoder = JSONDecoder()
do {
    let book = try decoder.decode(Book.self, from: data)

    print(book.title) // prints "A fairy tale"
    print(book.pages) // prints 100
    print(book.yearPublished) // prints 2000
    print(book.genre) // prints "comics"
    print(book.genre == .comics) // prints true
    print(book.genre == .fiction) // prints false
} catch {
    print(error.localizedDescription)
}

Coding Case Formats

1. lowercase – All in lower case letters, 
no spaces between words

2. UPPERCASE – All in capital letters,
no spaces between words

3. lowerCamelCase – or simply camelCase,
first word starts with lower case,
succeeding words starts with a capital letter

4. UpperCamelCase – or also known as the PascalCase,
first word starts with a capital letter,
succeeding words starts with a capital letter

5. snake_case – All words in lower case,
succeeding words are separated with an underscore

6. spinal-case – All words in lower case,
succeeding words are separated with a dash/hyphen

Apple made a documentation about Swift’s API Design Guidelines and it includes a guideline about following case conventions.

The case conventions for Swift is pretty simple and straightforward:

Names of types and protocols are UpperCamelCase. Everything else is lowerCamelCase.

Following this name case instruction, we should name our stored properties in lowerCamelCase.

/// Names of Types such as `Book` 
/// should be in UpperCamelCase
struct Book: Codable {

    /// Everything else should be in
    /// lowerCamelCase
    var title: String
    var pages: Int
    var yearPublished: Int
    var author: Author
}

JSON, on the other hand, doesn’t impose any restrictions or conventions for the use of case formats in names (ECMA-404).

The JSON syntax does not impose any restrictions on the strings used as names, does not require that name strings be unique, and does not assign any significance to the ordering of name/value pairs.

However, a lot of backend technologies might encode JSON keys differently in either lowerCamelCase or snake_case https://stackoverflow.com/a/25368854.

Example of a JSON encoded in lowerCamelCase:

{
    "title": "A fairy tale",
    "pages": null,
    "yearPublished": 2000,
    "author": {
        "name": "John Appleseed",
        "website": "https://johnappleseed.com"   
    }
}

Example of a JSON encoded in snake_case:

{
    "title": "A fairy tale",
    "pages": null,
    "year_published": 2000,
    "author": {
        "name": "John Appleseed",
        "website": "https://johnappleseed.com"   
    }
}

Working with lowerCamelCase is easy with Codable since the case format for both JSON and the Codable Swift model are equal, so there would be no changes needed.

However, the problem arises when we need to support parsing JSON encoded in snake_case, as we need to make sure they are decoded into lowerCamelCase in which our Codable Swift models are written into.

Coding snake_case format

The good news is that both JSONEncoder and JSONDecoder has a built-in snake_case format that we can use for its key encoding and decoding strategy.

/// Encodes a Codable Swift model from its
/// lowerCameCase format
/// into JSON in snake_case format
let jsonEncoder = JSONEncoder()
jsonEncoder.keyEncodingStrategy = .convertToSnakeCase

/// Decodes JSON in snake_case format
/// into a Codable Swift model in lowerCamelCase format
let jsonDecoder = JSONDecoder()
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase

Usage example:

/// A JSON in snake_case
let json = """
{
    "title": "A fairy tale",
    "pages": 100,
    "year_published": 2000,
    "genre": "comics"
}
"""

let data = Data(json.utf8)

/// We'll use a JSON decoder with a
/// snake_case key decoding strategy
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

do {
    let book = try decoder.decode(Book.self, from: data)
} catch {
    print(error.localizedDescription)
}

/// Encoding into JSON snake_case on the other hand:

/// First create a JSON encoder with a snake_case
/// key encoding strategy:
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase

/// Then encode a Codable Swift model with it:
let jsonData: Data = try encoder.encode(book)

/// You can also check it out in the console:
let jsonString = String(data: jsonData, encoding: .utf8)
print(jsonString)

Working with Dates

Dates in JSON can be represented as a String such as in ISO 8601 format: 2023-07-01T00:00:00.000Z, or as a long number in Unix epoch time: 1690266025.

ISO 8601

Dates that are in ISO 8601 format are written in String. For example, the date string: 2023-07-01T00:00:00.000Z means it is 01 July 2023, at 12AM UTC.

The ISO 8601 format starts with the date declaration (Year-Month-Day) which is why 01 July 2023 is written as 2023-07-01.

The T is a constant which signifies that the value that appears after it is the time. Time is declared in (hour:minute:seconds.milliseconds) which is whyT00:00:000.000Z is exactly at 12 midnight, and Z refers to Zulu-time or UTC (+0 GMT).

Now that we know what the ISO 8601 format means, let’s try and work on some example JSON.

{
    "title": "A fairy tale",
    "pages": null,
    "date_published": "2023-07-01T00:00:00Z",
    "author": {
        "name": "John Appleseed",
        "website": "https://johnappleseed.com"   
    }
}

The example above is the same JSON that represents a Book data, but this time we’ve changed the year_published property into a date_published property which is a String that contains a date in ISO 8601 format.

Let’s do the same to our Book struct by refactoring the name and type of yearPublished: Int into datePublished: Date.

struct Book: Codable {
    var title: String
    var pages: Int
    var datePublished: Date
    var author: Author
}

The great news is that JSONEncoder and JSONDecoder has built-in support for encoding and decoding dates in ISO 8601 format:

let json = """
{
    "title": "A fairy tale",
    "pages": 100,
    "date_published": "2023-07-01T00:00:00Z",
    "author": {
        "name": "John Appleseed",
        "website": "https://johnappleseed.com"   
    }
}
"""

let data = Data(json.utf8)

/// We'll use a JSON decoder with a
/// snake_case key decoding strategy
/// since date_published is in snake_case format
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

/// then we'll specify the date decoding strategy
/// to be ISO 8601:
decoder.dateDecodingStrategy = .iso8601

do {
    /// JSON: `date_published` as a date string
    /// will be mapped into
    /// Book.datePublished as a `Date` object
    let book = try decoder.decode(Book.self, from: data)
    print(book.datePublished)
} catch {
    print(error.localizedDescription)
}

/// Encoding example:
/// First create a JSON encoder with a snake_case
/// key encoding strategy
/// so Book.datePublished (lowerCamelCase) will be
/// encoded into `date_published` in snake_case
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
encoder.dateEncodingStrategy = .iso8601

/// Then encode a Codable Swift model with it:
let jsonData: Data = try encoder.encode(book)

/// You can also check it out in the console:
let jsonString = String(data: jsonData, encoding: .utf8)
print(jsonString)

Unix Epoch

Unix epoch dates are written in long numbers, as it is the number of seconds that have elapsed since 1 Jan 1970 00:00:00 UTC.

{
    "title": "A fairy tale",
    "pages": null,
    "date_published": 1690266025,
    "author": {
        "name": "John Appleseed",
        "website": "https://johnappleseed.com"   
    }
}

Similar to how we handled ISO 8601 dates, we can also convert Unix epoch dates to a native Swift Date object by using the built-in date coding strategies of both JSONEncoder and JSONDecoder.

struct Book: Codable {
    var title: String
    var pages: Int

    // Regardless of how the date is represented
    // from the JSON, we should always declare
    // the date data with the `Date` Swift type
    var datePublished: Date
    var author: Author
}

let json = """
{
    "title": "A fairy tale",
    "pages": 100,
    "date_published": "2023-07-01T00:00:00Z",
    "author": {
        "name": "John Appleseed",
        "website": "https://johnappleseed.com"   
    }
}
"""

let data = Data(json.utf8)

/// Decoding:
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
/// Specify the date decoding strategy to be:
decoder.dateDecodingStrategy = .secondsSince1970

do {
    /// JSON: `date_published` as a Unix epoch time
    /// will be mapped into
    /// Book.datePublished as a `Date` object
    let book = try decoder.decode(Book.self, from: data)
    print(book.datePublished)
} catch {
    print(error.localizedDescription)
}

/// Encoding:
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
/// Specify this encoding strategy to encode to
/// Unix time
encoder.dateEncodingStrategy = .secondsSince1970

/// Then encode a Codable Swift model with it:
let jsonData: Data = try encoder.encode(book)

/// You can also check it out in the console:
let jsonString = String(data: jsonData, encoding: .utf8)
print(jsonString)

ISO 8601 and Unix epoch time are the most commonly used formats to represent date and time data in JSON, and by using built-in coding strategies, we can make encoding and decoding pretty easy.

For date/time formats that are non-standard, we can use the date coding strategy .formatted(DateFormatter) which accepts a DateFormatter. However, you should be careful on using this as the date format is used every single time you encode/decode objects, which may also impact performance if not done well.

This concludes the Codable protocol series. I hope that you’ve learned a lot from reading this article.

Table of contents
    0 Shares
    Share
    Tweet
    Pin
    Share
    Buffer