Decoding and Encoding Nested JSON in Swift Using Codable Decoding and Encoding Nested JSON Using Codable

Working with nested JSON data is a common task when developing iOS applications. In Swift, the Codable protocol simplifies the process of decoding JSON data into native Swift objects. In this article, we'll explore how to decode nested JSON structures in Swift using the Codable protocol.

Understanding Nested JSON

Nested JSON, as the name suggests, involves JSON objects or arrays within other JSON objects or arrays. These nested structures can quickly become complex, especially when dealing with real-world data. To decode such data efficiently, we need to create corresponding Swift structures that match the JSON's hierarchical structure.

Creating Swift Structures

The first step is to define Swift structures that mirror the JSON data's structure. Each structure should conform to the Codable protocol and include properties that match the keys in the JSON.

Here's an example of a nested JSON structure representing a blog post with comments:

{
    "title": "Sample Blog Post",
    "content": "This is the content of the blog post.",
    "date_published": "2023-10-06T08:00:00Z",
    "comments": [
        {
            "text": "Great post!",
            "date": "2023-10-06T10:00:00Z",
            "user": {
                "username": "user1",
                "email": "user1@example.com"
            }
        },
        {
            "text": "Thanks for sharing.",
            "date": "2023-10-06T11:00:00Z",
            "user": {
                "username": "user2",
                "email": "user2@example.com"
            }
        }
    ]
}

We can create Swift structures like this:

import Foundation
struct User: Codable {
    var username: String
    var email: String
}
struct Comment: Codable {
    var text: String
    var date: Date
    var user: User
}
struct BlogPost: Codable {
    var title: String
    var content: String
    var datePublished: Date
    var comments: [Comment]
    enum CodingKeys: String, CodingKey {
        case title
        case content
        case datePublished = "date_published"
        case comments
    }
}

In the BlogPost structure, we've used the CodingKeys enum to map JSON keys to Swift property names. We also use nested structures (User and Comment )to represent the JSON hierarchy.

Encode & Decode Manually

If the structure of your Swift type differs from the structure of its encoded form, you can provide a custom implementation of Encodable and Decodable to define your own encoding and decoding logic.

Decode

In the init(from decoder: Decoder) throws method of the BlogPost structure, we implement the decoding logic for our Swift object from the JSON data. This method is required when conforming to the Decodable protocol (part of Codable ).

Let's break down this method step by step:

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        title = try container.decode(String.self, forKey: .title)
        content = try container.decode(String.self, forKey: .content)
        datePublished = try container.decode(Date.self, forKey: .datePublished)
        var commentsContainer = try container.nestedUnkeyedContainer(forKey: .comments)
        var commentsArray: [Comment] = []
        while !commentsContainer.isAtEnd {
            let commentContainer = try commentsContainer.nestedContainer(keyedBy: CommentCodingKeys.self)
            let text = try commentContainer.decode(String.self, forKey: .text)
            let date = try commentContainer.decode(Date.self, forKey: .date)
            let user = try commentContainer.decode(User.self, forKey: .user)
            let comment = Comment(text: text, date: date, user: user)
            commentsArray.append(comment)
        }
        comments = commentsArray
    }
  1. Container Initialization: We start by initializing a container using try decoder.container(keyedBy: CodingKeys.self) .This container represents the top-level keys of the JSON object. CodingKeys is an enum we define within the BlogPost structure to specify the keys we expect in the JSON.
  2. Decoding Simple Properties: Next, we decode the simple properties (title ,content ,and datePublished )using try container.decode(Type.self, forKey: .key) .This extracts values from the JSON and assigns them to our Swift properties.
  3. Nested Container for Comments: The comments property in our BlogPost structure is an array of Comment objects. Since comments are nested within the JSON, we create a nested container using try container.nestedUnkeyedContainer(forKey: .comments) .This container represents an array of comments in the JSON.
  4. Iterating Through Comments: We initialize an empty array, commentsArray ,to store the decoded comments. Then, we enter a loop with while !commentsContainer.isAtEnd to iterate through each comment within the JSON.
  5. Decoding Comment Properties: Within the loop, we create another nested container, commentContainer ,using try commentsContainer.nestedContainer(keyedBy: CommentCodingKeys.self) .This container represents an individual comment within the array.
  6. Decode Comment Properties: We decode the properties of the comment (text ,date ,and user )from commentContainer .For example, let text = try commentContainer.decode(String.self, forKey: .text) extracts the comment text.
  7. Creating Comment Objects: With the decoded properties, we create a Comment object and append it to the commentsArray .
  8. Finalizing Comments: After the loop, we assign the commentsArray to the comments property, effectively populating our BlogPost object with the decoded comments.

This process repeats for each comment in the JSON array, allowing us to build a fully populated BlogPost object with all of its nested data.

The init(from decoder: Decoder) method demonstrates how the Codable protocol's power lies in its ability to handle complex, nested data structures with ease, making it a valuable tool for parsing JSON in Swift applications.

Encode

In the func encode(to encoder: Encoder) throws method of the BlogPost structure, we implement the encoding logic for our Swift object into JSON format. This method is required when conforming to the Encodable protocol (part of Codable ).Let's break down this method step by step:

func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(title, forKey: .title)
        try container.encode(content, forKey: .content)
        try container.encode(datePublished, forKey: .datePublished)
        var commentsContainer = container.nestedUnkeyedContainer(forKey: .comments)
        for comment in comments {
            var commentContainer = commentsContainer.nestedContainer(keyedBy: CommentCodingKeys.self)
            try commentContainer.encode(comment.text, forKey: .text)
            try commentContainer.encode(comment.date, forKey: .date)
            try commentContainer.encode(comment.user, forKey: .user)
        }
    }
  1. Container Initialization: We start by initializing a container using var container = encoder.container(keyedBy: CodingKeys.self) .This container represents the top-level keys of the JSON object. CodingKeys is an enum we define within the BlogPost structure to specify the keys we want in the JSON output.
  2. Encoding Simple Properties: We encode the simple properties (title ,content ,and datePublished )into the container using try container.encode(value, forKey: .key) .This takes the values stored in our Swift properties and adds them to the JSON output.
  3. Nested Container for Comments: The comments property in our BlogPost structure is an array of Comment objects. Since comments are nested within the JSON, we create a nested container using var commentsContainer = container.nestedUnkeyedContainer(forKey: .comments) .This container represents an array of comments in the JSON.
  4. Iterating Through Comments: We enter a loop to iterate through each comment within the comments array of our Swift object. This allows us to encode each comment individually.
  5. Creating Nested Comment Container: Within the loop, we create another nested container, commentContainer ,using var commentContainer = commentsContainer.nestedContainer(keyedBy: CommentCodingKeys.self) .This container represents an individual comment within the array.
  6. Encoding Comment Properties: We encode the properties of the comment (text ,date ,and user )into commentContainer using try commentContainer.encode(value, forKey: .key) .For example, try commentContainer.encode(comment.text, forKey: .text) adds the comment text to the JSON.
  7. Finalizing Comments: After encoding all the comments in the loop, the JSON representation of the comments array is complete.

This process repeats for each comment in the Swift array, resulting in a fully encoded JSON representation of our BlogPost object, including its nested data.

The func encode(to encoder: Encoder) method showcases the versatility of the Codable protocol, allowing us to easily convert complex Swift data structures into JSON. This is particularly useful when sending data to APIs, saving data to a file, or any other scenario where JSON serialization is required in Swift applications.

Decoding Nested JSON

To decode nested JSON, you need to use a JSONDecoder and specify how to decode the data. Set the decoder's dateDecodingStrategy to handle date formats (e.g., ISO8601) and keyDecodingStrategy to convert snake_case keys to camelCase, if necessary. Once you've successfully decoded the JSON into Swift structures, you can access the nested data as you would with any Swift object.

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
    let blogPost = try decoder.decode(BlogPost.self, from: jsonData)
    print("Title: \(blogPost.title)")
    print("Content: \(blogPost.content)")
    print("Date Published: \(blogPost.datePublished)")
    print("Comments:")
    for comment in blogPost.comments {
        print("  Text: \(comment.text)")
        print("  Date: \(comment.date)")
        print("  User: \(comment.user.username) (\(comment.user.email))")
        print("-----")
    }
} catch {
    print("Error decoding JSON: \(error)")
}
Encoding Swift Objects to JSON

You can also encode Swift objects back into JSON using the JSONEncoder .This is useful when you need to send data to a server or store it as JSON.

let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
encoder.keyEncodingStrategy = .convertToSnakeCase
do {
    let jsonData = try encoder.encode(blogPost)
    let jsonString = String(data: jsonData, encoding: .utf8)
    // jsonString contains the JSON representation of the blogPost
} catch {
    print("Error encoding JSON: \(error)")
}
Conclusion

Decoding and encoding nested JSON data in Swift is made straightforward and efficient thanks to the Codable protocol. By defining corresponding Swift structures, utilizing JSONDecoder for decoding, and JSONEncoder for encoding, you can seamlessly handle complex JSON hierarchies and work with the data in a type-safe manner within your iOS applications.