Swift Codable: Handling JSON Like a Pro

Master the art of data encoding and decoding in Swift with Codable.

Vikram Kumar
11 min readOct 25, 2023

Introduction

Swift Codable is a powerful framework that simplifies the process of converting between your Swift objects and external data representations, such as JSON. Whether you’re working with APIs, saving data to disk, or exchanging information with a server, Codable streamlines the data serialization and deserialization process.

In this article, we’ll dive deep into Swift Codable, covering its core concepts and providing practical examples that demonstrate its versatility and ease of use.

Photo by Martin Shreder on Unsplash

Understanding Codable Basics

What Is Codable?

Codable is a protocol in Swift that combines two other protocols: Encodable and Decodable. These protocols provide a standard way to encode (serialize) and decode (deserialize) data in various formats, such as JSON or Property Lists.

/// A type that can convert itself into and out of an external representation.
///
/// `Codable` is a type alias for the `Encodable` and `Decodable` protocols.
/// When you use `Codable` as a type or a generic constraint, it matches
/// any type that conforms to both protocols.
public typealias Codable = Decodable & Encodable

Codable Key Components

  1. Encodable: Conforming to the Encodable protocol allows you to convert your Swift objects into an external representation (e.g., JSON).
  2. Decodable: Conforming to the Decodable protocol enables you to convert external representations (e.g., JSON) back into Swift objects.

Practical Examples

Let’s explore practical scenarios where Codable can simplify your Swift development.

1. Encoding to JSON

Suppose you have a Swift struct representing a Person:

struct Person: Codable {
let name: String
let age: Int
let email: String
}

You can effortlessly encode this struct to JSON:

let person = Person(name: "Alice", age: 30, email: "alice@example.com")

do {
let jsonData = try JSONEncoder().encode(person)
let jsonString = String(data: jsonData, encoding: .utf8)
print(jsonString ?? "Unable to convert to string")
} catch {
print("Encoding failed: \(error)")
}

This code creates a Person instance, encodes it into JSON data using JSONEncoder, and then converts the data to a readable JSON string.

2. Decoding from JSON

Now, let’s decode JSON data back into a Swift struct:

let jsonString = """
{
"name": "Bob",
"age": 25,
"email": "bob@example.com"
}
"""

if let jsonData = jsonString.data(using: .utf8) {
do {
let person = try JSONDecoder().decode(Person.self, from: jsonData)
print(person)
} catch {
print("Decoding failed: \(error)")
}
}

This code takes a JSON string, converts it to data, and then uses JSONDecoder to decode it into a Person struct. The result is a Swift object that you can work with.

3. Handling Arrays

Codable is equally adept at handling arrays of objects. Suppose you have a JSON array representing a list of people:

let jsonArray = """
[
{
"name": "Carol",
"age": 28,
"email": "carol@example.com"
},
{
"name": "David",
"age": 35,
"email": "david@example.com"
}
]
"""

You can decode this array into an array of Person objects:

if let jsonData = jsonArray.data(using: .utf8) {
do {
let people = try JSONDecoder().decode([Person].self, from: jsonData)
print(people)
} catch {
print("Decoding failed: \(error)")
}
}

This code parses the JSON array into an array of Person objects, demonstrating Codable's ability to handle complex data structures.

4. Nested Objects

Codable can also handle nested objects. Suppose you have two Swift structs: Author and Book, where an Author can have multiple Book objects, and you want to encode and decode this nested relationship.

struct Author: Codable {
let name: String
let nationality: String
let books: [Book]
}

struct Book: Codable {
let title: String
let publicationYear: Int
}

Encoding to JSON

To encode an Author object with nested Book objects to JSON:

let book1 = Book(title: "Swift 101", publicationYear: 2020)
let book2 = Book(title: "Advanced Swift", publicationYear: 2021)

let author = Author(name: "John Doe", nationality: "American", books: [book1, book2])

do {
let jsonData = try JSONEncoder().encode(author)
let jsonString = String(data: jsonData, encoding: .utf8)
print(jsonString ?? "Unable to convert to string")
} catch {
print("Encoding failed: \(error)")
}

This code creates an Author with two nested Book objects, encodes the entire structure to JSON, and converts the JSON data to a string.

Decoding from JSON

To decode JSON data back into Swift objects with nested relationships:

let jsonString = """
{
"name": "Jane Smith",
"nationality": "British",
"books": [
{
"title": "Intro to Swift",
"publicationYear": 2019
},
{
"title": "Swift Mastery",
"publicationYear": 2022
}
]
}
"""

if let jsonData = jsonString.data(using: .utf8) {
do {
let author = try JSONDecoder().decode(Author.self, from: jsonData)
print(author)
} catch {
print("Decoding failed: \(error)")
}
}

This code takes a JSON string representing an Author with nested Book objects, converts it to data, and uses JSONDecoder to decode it into Swift objects.

Codable’s ability to handle nested objects simplifies working with complex data structures, such as JSON responses from APIs or complex data models within your Swift applications. It streamlines the serialization and deserialization process, making your code more efficient and maintainable.

Codable for API Integration

Using Swift Codable for API integration is a powerful and efficient way to work with external data in your iOS apps. In this example, we’ll demonstrate how to fetch and decode JSON data from a hypothetical API using Codable.

Creating the Data Model

First, you’ll define a data model that corresponds to the structure of the JSON data you expect to receive from the API. In this example, we’ll use a Post struct to represent posts from a JSONPlaceholder API:

struct Post: Codable {
let userId: Int?
let id: Int?
let title: String?
let body: String?
}

Fetching and Decoding JSON Data

You can use Swift’s URLSession to make a network request to the API and then decode the JSON response using Codable:

guard let apiURL = URL(string: "https://jsonplaceholder.typicode.com/posts/1") else {
return
}

URLSession.shared.dataTask(with: apiURL) { data, response, error in
if let error = error {
print("Network request error: \(error)")
return
}

if let data = data {
do {
let post = try JSONDecoder().decode(Post.self, from: data)
print("Post Title: \(post.title)")
} catch {
print("JSON decoding error: \(error)")
}
}
}.resume()

In this code:

  1. We create a URL representing the API endpoint.
  2. We use URLSession to make a data task for the API URL.
  3. Inside the data task’s completion handler, we handle errors and then attempt to decode the received JSON data into a Post object using JSONDecoder.
  4. If decoding is successful, we print the title of the post.

By conforming to Codable, the Post struct knows how to map its properties to the JSON keys and allows seamless data conversion.

This example demonstrates how Codable streamlines API integration by handling JSON serialization and deserialization for you. You can adapt this pattern for various API endpoints and data models in your app, making network requests and data parsing clean and efficient.

CodingKeys

In Swift Codable, the CodingKeys enum is a fundamental component used to specify how Swift properties are mapped to keys in an external data representation, such as JSON. This enum allows you to customize the encoding and decoding process when the property names in your Swift type differ from the keys in the data you're working with.

Here’s how CodingKeys works:

  1. Defining the CodingKeys Enum: To customize the encoding and decoding process, you create an enum within your Codable type (usually a struct or class). This enum conforms to the CodingKey protocol and specifies how each property should be encoded or decoded.
  2. Mapping Properties: Inside the CodingKeys enum, you declare cases that match your Swift properties. Each case represents a property and assigns a raw value (String) that corresponds to the key used in the external data representation (e.g., JSON). These raw values effectively map your Swift property names to the external keys.
  3. Custom Encoding and Decoding: When encoding (serialization) or decoding (deserialization) instances of your Codable type, the CodingKeys enum guides the process. For encoding, it specifies which property values should be associated with which keys in the encoded data. For decoding, it tells Codable how to map keys in the data back to properties in your Swift type.

Here’s a simple example:

struct Person: Codable {
let firstName: String
let lastName: String
let age: Int

enum CodingKeys: String, CodingKey {
case firstName = "first_name"
case lastName = "last_name"
case age
}
}

In this Person struct, the CodingKeys enum customizes the property-key mapping. It specifies that the Swift firstName property should be encoded and decoded as "first_name" in the external data, the lastName property as "last_name," and the age property with the same key, "age."

Now, when you use this struct for encoding and decoding, Swift Codable will follow the mapping instructions provided by the CodingKeys enum.

This feature is powerful when you need to adapt your Swift code to work with data sources or APIs that use different naming conventions or key names. It allows you to seamlessly bridge the gap between your Swift types and external data representations.

Working With Date

Swift Codable provides built-in support for working with dates during the encoding and decoding process. When dealing with APIs or data formats that include date values, Codable can automatically convert between Swift Date objects and the corresponding date representations in the external data.

Here’s how Codable works with dates:

Encoding Dates

When encoding Swift objects with Date properties, you can use JSONEncoder to automatically convert these dates into the appropriate format (usually ISO 8601) in the JSON output. To do this, simply conform to the Codable protocol and use the default encoding settings:

import Foundation

struct Event: Codable {
let name: String
let date: Date
}

let event = Event(name: "Swift Conference", date: Date())

do {
let jsonData = try JSONEncoder().encode(event)
let jsonString = String(data: jsonData, encoding: .utf8)
print(jsonString ?? "Unable to convert to string")
} catch {
print("Encoding failed: \(error)")
}

In this example, the Date property date is automatically encoded to a format like "2023-10-19T15:30:00Z".

Decoding Dates

When decoding external data with date values into Swift objects, Codable can parse the date representations in the JSON and convert them into Swift Date objects. Again, conform to the Codable protocol, and use the default decoding settings:

import Foundation

let jsonString = """
{
"name": "Swift Conference",
"date": "2023-10-19T15:30:00Z"
}
"""

if let jsonData = jsonString.data(using: .utf8) {
do {
let event = try JSONDecoder().decode(Event.self, from: jsonData)
print(event.date)
} catch {
print("Decoding failed: \(error)")
}
}

In this example, Codable recognizes the date string in the JSON and automatically converts it to a Date object.

Custom Date Formats

Custom date formats in Swift Codable allow you to work with date values in a format that’s specific to your data source. By defining a custom date formatter and configuring the date encoding and decoding strategies, you can ensure that your Swift objects correctly serialize and deserialize dates with the desired format.

Here’s how to set up custom date formats in Swift Codable:

Custom Date Formatter

First, create a custom date formatter using the DateFormatter class. This formatter defines the format that dates should be represented in your external data source (API):

let customDateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "dd/MM/yyyy" // Your desired date format
return formatter
}()

Decoding Custom Date Format

When decoding external data with custom date formats into Swift objects, configure the JSONDecoder to use your custom date formatter as the date decoding strategy:

let jsonString = """
{
"name": "Custom Event",
"date": "19/10/2023"
}
"""

if let jsonData = jsonString.data(using: .utf8) {
do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(customDateFormatter)

let event = try decoder.decode(Event.self, from: jsonData)
print(event.date)
} catch {
print("Decoding failed: \(error)")
}
}

In this example, we set the dateDecodingStrategy of the JSONDecoder to .formatted(customDateFormatter). This instructs the decoder to use your custom date formatter to decode the date from the JSON string.

Custom date formats in Swift Codable enable you to handle dates in a way that matches your specific data source, ensuring correct serialization and deserialization of date values in your Swift applications.

Handle null or missing values

Handling null or missing values when working with Codable in Swift is a common scenario, especially when dealing with external data sources, like APIs, where fields may not always be present. You can handle null values effectively using optionals and some customization. Here’s how you can do it:

Handling null or missing values when working with Codable in Swift is a common scenario, especially when dealing with external data sources, like APIs, where fields may not always be present. You can handle null values effectively using optionals and some customization. Here’s how you can do it:

Using Optionals:

  1. Declare optional properties in your Codable struct or class for fields that can be missing in the data.
  2. Codable will automatically set optional properties to nil if the corresponding key is missing in the JSON.
struct User: Codable {
let id: Int
let name: String?
}

In this example, the name property is optional, so it can handle the absence of the "name" key in the JSON data.

Default Values:

  1. You can set default values for properties that should have a value when the key is missing.
  2. In this case, you should use a custom init(from:) method to assign the default value if the key is not present.
struct User: Codable {
let id: Int
let name: String

enum CodingKeys: String, CodingKey {
case id
case name = "full_name"
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id)
name = try container.decodeIfPresent(String.self, forKey: .name) ?? "Unknown"
}
}

In this example, if the “full_name” key is missing in the JSON, the name property will be assigned the default value "Unknown."

Conditional Handling:

  1. You can conditionally handle null values based on your application’s requirements.
  2. For example, if a missing field is an error condition, you can throw an error in the init(from:) method.
struct User: Codable {
let id: Int
let name: String

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id)

if let fullName = try? container.decode(String.self, forKey: .name) {
name = fullName
} else {
throw DecodingError.dataCorruptedError(forKey: .name, in: container, debugDescription: "Name is missing or null")
}
}
}

This example throws an error if the “name” field is missing or null in the JSON.

By using optionals, default values, and custom initialization, you can effectively handle null or missing values when working with Codable in Swift, making your data decoding more robust and flexible.

Codable Best Practices

When working with Codable in Swift, there are several best practices you should follow to ensure smooth encoding and decoding of your data. Here are some Codable best practices:

  1. Conform to Codable: Ensure your data structures (structs or classes) conform to the Codable protocol. By default, this protocol includes both Encodable and Decodable, allowing you to both encode and decode data.
  2. CodingKeys for Custom Key Mapping: Use the CodingKeys enum to map your Swift property names to the keys in your external data source. This is especially useful when the property names in your Swift type differ from the keys in the JSON data.
enum CodingKeys: String, CodingKey {
case firstName = "first_name"
case lastName = "last_name"
}

3. Use Date Strategies: If your data includes date values, set appropriate date encoding and decoding strategies. Swift provides .iso8601 as the default, but you can customize date formats as needed.

let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
JSONEncoder().dateEncodingStrategy = .formatted(dateFormatter)
JSONDecoder().dateDecodingStrategy = .formatted(dateFormatter)

4. Customize Encoding and Decoding: Codable provides customization points for both encoding and decoding. You can implement encode(to:) and init(from:) methods to add custom behavior.

5. Handle Optionals: Use optionals for properties that may not always be present in your data. This is especially helpful when dealing with incomplete or changing data.

6. Error Handling: Wrap your encoding and decoding code in do-catch blocks to handle potential errors gracefully. Swift's error handling mechanisms are your friend when dealing with potentially erroneous data.

do {
let decodedData = try JSONDecoder().decode(MyStruct.self, from: jsonData)
} catch {
print("Decoding error: \(error)")
}

7. Testing: Always test your encoding and decoding logic thoroughly. Ensure it works as expected in various scenarios, including edge cases.

8. Validation: Perform validation after decoding, ensuring that the data is in a valid state. This includes checking for required fields, ranges, and constraints.

9. Performance Considerations: Codable provides great convenience, but for ultra-high-performance scenarios, you might want to explore more efficient serialization libraries. Codable can be slower in some cases.

10. Versioning: If your data structures may change over time, consider versioning your Codable types. This allows you to evolve your data structures without breaking existing code.

By following these best practices, you can leverage the power of Swift Codable for efficient and reliable data encoding and decoding in your applications. It ensures that your data handling is both robust and maintainable, even in complex scenarios.

Conclusion

Swift Codable is a powerful and versatile framework for encoding and decoding data in your Swift applications. It simplifies the process of working with various data formats, such as JSON, and allows your Swift types to seamlessly interact with external data sources.

Happy Coding!!!

--

--

Vikram Kumar
Vikram Kumar

Written by Vikram Kumar

I am Vikram, a Senior iOS Developer at Matellio Inc. focused on writing clean and efficient code. Complex problem-solver with an analytical and driven mindset.

No responses yet