How to decode a nested JSON struct with Swift Decodable protocol? How to decode a nested JSON struct with Swift Decodable protocol? swift swift

How to decode a nested JSON struct with Swift Decodable protocol?


Another approach is to create an intermediate model that closely matches the JSON (with the help of a tool like quicktype.io), let Swift generate the methods to decode it, and then pick off the pieces that you want in your final data model:

// snake_case to match the JSON and hence no need to write CodingKey enums / structfileprivate struct RawServerResponse: Decodable {    struct User: Decodable {        var user_name: String        var real_info: UserRealInfo    }    struct UserRealInfo: Decodable {        var full_name: String    }    struct Review: Decodable {        var count: Int    }    var id: Int    var user: User    var reviews_count: [Review]}struct ServerResponse: Decodable {    var id: String    var username: String    var fullName: String    var reviewCount: Int    init(from decoder: Decoder) throws {        let rawResponse = try RawServerResponse(from: decoder)        // Now you can pick items that are important to your data model,        // conveniently decoded into a Swift structure        id = String(rawResponse.id)        username = rawResponse.user.user_name        fullName = rawResponse.user.real_info.full_name        reviewCount = rawResponse.reviews_count.first!.count    }}

This also allows you to easily iterate through reviews_count, should it contain more than 1 value in the future.


In order to solve your problem, you can split your RawServerResponse implementation into several logic parts (using Swift 5).


#1. Implement the properties and required coding keys

import Foundationstruct RawServerResponse {    enum RootKeys: String, CodingKey {        case id, user, reviewCount = "reviews_count"    }    enum UserKeys: String, CodingKey {        case userName = "user_name", realInfo = "real_info"    }    enum RealInfoKeys: String, CodingKey {        case fullName = "full_name"    }    enum ReviewCountKeys: String, CodingKey {        case count    }    let id: Int    let userName: String    let fullName: String    let reviewCount: Int}

#2. Set the decoding strategy for id property

extension RawServerResponse: Decodable {    init(from decoder: Decoder) throws {        // id        let container = try decoder.container(keyedBy: RootKeys.self)        id = try container.decode(Int.self, forKey: .id)        /* ... */                     }}

#3. Set the decoding strategy for userName property

extension RawServerResponse: Decodable {    init(from decoder: Decoder) throws {        /* ... */        // userName        let userContainer = try container.nestedContainer(keyedBy: UserKeys.self, forKey: .user)        userName = try userContainer.decode(String.self, forKey: .userName)        /* ... */    }}

#4. Set the decoding strategy for fullName property

extension RawServerResponse: Decodable {    init(from decoder: Decoder) throws {        /* ... */        // fullName        let realInfoKeysContainer = try userContainer.nestedContainer(keyedBy: RealInfoKeys.self, forKey: .realInfo)        fullName = try realInfoKeysContainer.decode(String.self, forKey: .fullName)        /* ... */    }}

#5. Set the decoding strategy for reviewCount property

extension RawServerResponse: Decodable {    init(from decoder: Decoder) throws {        /* ...*/                // reviewCount        var reviewUnkeyedContainer = try container.nestedUnkeyedContainer(forKey: .reviewCount)        var reviewCountArray = [Int]()        while !reviewUnkeyedContainer.isAtEnd {            let reviewCountContainer = try reviewUnkeyedContainer.nestedContainer(keyedBy: ReviewCountKeys.self)            reviewCountArray.append(try reviewCountContainer.decode(Int.self, forKey: .count))        }        guard let reviewCount = reviewCountArray.first else {            throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: container.codingPath + [RootKeys.reviewCount], debugDescription: "reviews_count cannot be empty"))        }        self.reviewCount = reviewCount    }}

Complete implementation

import Foundationstruct RawServerResponse {    enum RootKeys: String, CodingKey {        case id, user, reviewCount = "reviews_count"    }    enum UserKeys: String, CodingKey {        case userName = "user_name", realInfo = "real_info"    }    enum RealInfoKeys: String, CodingKey {        case fullName = "full_name"    }    enum ReviewCountKeys: String, CodingKey {        case count    }    let id: Int    let userName: String    let fullName: String    let reviewCount: Int}
extension RawServerResponse: Decodable {    init(from decoder: Decoder) throws {        // id        let container = try decoder.container(keyedBy: RootKeys.self)        id = try container.decode(Int.self, forKey: .id)        // userName        let userContainer = try container.nestedContainer(keyedBy: UserKeys.self, forKey: .user)        userName = try userContainer.decode(String.self, forKey: .userName)        // fullName        let realInfoKeysContainer = try userContainer.nestedContainer(keyedBy: RealInfoKeys.self, forKey: .realInfo)        fullName = try realInfoKeysContainer.decode(String.self, forKey: .fullName)        // reviewCount        var reviewUnkeyedContainer = try container.nestedUnkeyedContainer(forKey: .reviewCount)        var reviewCountArray = [Int]()        while !reviewUnkeyedContainer.isAtEnd {            let reviewCountContainer = try reviewUnkeyedContainer.nestedContainer(keyedBy: ReviewCountKeys.self)            reviewCountArray.append(try reviewCountContainer.decode(Int.self, forKey: .count))        }        guard let reviewCount = reviewCountArray.first else {            throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: container.codingPath + [RootKeys.reviewCount], debugDescription: "reviews_count cannot be empty"))        }        self.reviewCount = reviewCount    }}

Usage

let jsonString = """{    "id": 1,    "user": {        "user_name": "Tester",        "real_info": {            "full_name":"Jon Doe"        }    },    "reviews_count": [    {    "count": 4    }    ]}"""let jsonData = jsonString.data(using: .utf8)!let decoder = JSONDecoder()let serverResponse = try! decoder.decode(RawServerResponse.self, from: jsonData)dump(serverResponse)/*prints:▿ RawServerResponse #1 in __lldb_expr_389  - id: 1  - user: "Tester"  - fullName: "Jon Doe"  - reviewCount: 4*/


Rather than having one big CodingKeys enumeration with all the keys you'll need for decoding the JSON, I would advise splitting the keys up for each of your nested JSON objects, using nested enumerations to preserve the hierarchy:

// top-level JSON object keysprivate enum CodingKeys : String, CodingKey {    // using camelCase case names, with snake_case raw values where necessary.    // the raw values are what's used as the actual keys for the JSON object,    // and default to the case name unless otherwise specified.    case id, user, reviewsCount = "reviews_count"    // "user" JSON object keys    enum User : String, CodingKey {        case username = "user_name", realInfo = "real_info"        // "real_info" JSON object keys        enum RealInfo : String, CodingKey {            case fullName = "full_name"        }    }    // nested JSON objects in "reviews" keys    enum ReviewsCount : String, CodingKey {        case count    }}

This will make it easier to keep track of the keys at each level in your JSON.

Now, bearing in mind that:

  • A keyed container is used to decode a JSON object, and is decoded with a CodingKey conforming type (such as the ones we've defined above).

  • An unkeyed container is used to decode a JSON array, and is decoded sequentially (i.e each time you call a decode or nested container method on it, it advances to the next element in the array). See the second part of the answer for how you can iterate through one.

After getting your top-level keyed container from the decoder with container(keyedBy:) (as you have a JSON object at the top-level), you can repeatedly use the methods:

For example:

struct ServerResponse : Decodable {    var id: Int, username: String, fullName: String, reviewCount: Int    private enum CodingKeys : String, CodingKey { /* see above definition in answer */ }    init(from decoder: Decoder) throws {        // top-level container        let container = try decoder.container(keyedBy: CodingKeys.self)        self.id = try container.decode(Int.self, forKey: .id)        // container for { "user_name": "Tester", "real_info": { "full_name": "Jon Doe" } }        let userContainer =            try container.nestedContainer(keyedBy: CodingKeys.User.self, forKey: .user)        self.username = try userContainer.decode(String.self, forKey: .username)        // container for { "full_name": "Jon Doe" }        let realInfoContainer =            try userContainer.nestedContainer(keyedBy: CodingKeys.User.RealInfo.self,                                              forKey: .realInfo)        self.fullName = try realInfoContainer.decode(String.self, forKey: .fullName)        // container for [{ "count": 4 }] – must be a var, as calling a nested container        // method on it advances it to the next element.        var reviewCountContainer =            try container.nestedUnkeyedContainer(forKey: .reviewsCount)        // container for { "count" : 4 }        // (note that we're only considering the first element of the array)        let firstReviewCountContainer =            try reviewCountContainer.nestedContainer(keyedBy: CodingKeys.ReviewsCount.self)        self.reviewCount = try firstReviewCountContainer.decode(Int.self, forKey: .count)    }}

Example decoding:

let jsonData = """{  "id": 1,  "user": {    "user_name": "Tester",    "real_info": {    "full_name":"Jon Doe"  }  },  "reviews_count": [    {      "count": 4    }  ]}""".data(using: .utf8)!do {    let response = try JSONDecoder().decode(ServerResponse.self, from: jsonData)    print(response)} catch {    print(error)}// ServerResponse(id: 1, username: "Tester", fullName: "Jon Doe", reviewCount: 4)

Iterating through an unkeyed container

Considering the case where you want reviewCount to be an [Int], where each element represents the value for the "count" key in the nested JSON:

  "reviews_count": [    {      "count": 4    },    {      "count": 5    }  ]

You'll need to iterate through the nested unkeyed container, getting the nested keyed container at each iteration, and decoding the value for the "count" key. You can use the count property of the unkeyed container in order to pre-allocate the resultant array, and then the isAtEnd property to iterate through it.

For example:

struct ServerResponse : Decodable {    var id: Int    var username: String    var fullName: String    var reviewCounts = [Int]()    // ...    init(from decoder: Decoder) throws {        // ...        // container for [{ "count": 4 }, { "count": 5 }]        var reviewCountContainer =            try container.nestedUnkeyedContainer(forKey: .reviewsCount)        // pre-allocate the reviewCounts array if we can        if let count = reviewCountContainer.count {            self.reviewCounts.reserveCapacity(count)        }        // iterate through each of the nested keyed containers, getting the        // value for the "count" key, and appending to the array.        while !reviewCountContainer.isAtEnd {            // container for a single nested object in the array, e.g { "count": 4 }            let nestedReviewCountContainer = try reviewCountContainer.nestedContainer(                                                 keyedBy: CodingKeys.ReviewsCount.self)            self.reviewCounts.append(                try nestedReviewCountContainer.decode(Int.self, forKey: .count)            )        }    }}