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

8

138

Here is my JSON

{
    "id": 1,
    "user": {
        "user_name": "Tester",
        "real_info": {
            "full_name":"Jon Doe"
        }
    },
    "reviews_count": [
        {
            "count": 4
        }
    ]
}

Here is the structure I want it saved to (incomplete)

struct ServerResponse: Decodable {
    var id: String
    var username: String
    var fullName: String
    var reviewCount: Int

    enum CodingKeys: String, CodingKey {
       case id, 
       // How do i get nested values?
    }
}

I have looked at Apple's Documentation on decoding nested structs, but I still do not understand how to do the different levels of the JSON properly. Any help will be much appreciated.

Bethune answered 14/6, 2017 at 15:47 Comment(2)
You simply "nest" it all the way down as classes, to match the structure. It's that easy.Gigahertz
TBC there is not a way in Swift to "nest it inline". You very simply take a new struct for each nest. Exactly as in @MojtabaHosseini answer. That's all there is to it.Gigahertz
S
150

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
fileprivate 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.

Soccer answered 14/6, 2017 at 23:23 Comment(0)
S
148

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 Foundation

struct 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 Foundation

struct 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
*/
Sitwell answered 11/7, 2017 at 20:52 Comment(0)
C
38

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 keys
private 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)
            )
        }
    }
}
Chitchat answered 14/6, 2017 at 16:12 Comment(4)
one thing to clarify: what did you mean by I would advise splitting the keys for each of your nested JSON objects up into multiple nested enumerations, thereby making it easier to keep track of the keys at each level in your JSON ?Bethune
@JTAppleCalendarforiOSSwift I mean that rather than having one big CodingKeys enum with all the keys you'll need to decode your JSON object, you should split them up into multiple enums for each JSON object – for example, in the above code we have CodingKeys.User with the keys to decode the user JSON object ({ "user_name": "Tester", "real_info": { "full_name": "Jon Doe" } }), so just the keys for "user_name" & "real_info".Chitchat
I had one question about the reviews_count which is an array of dictionary. Currently, the code works as expected. My reviewsCount only ever has one value in the array. But what if i actually wanted an array of review_count, then I'd need to simply declare var reviewCount: Int as an array right? -> var reviewCount: [Int]. And then i'd need to also edit the ReviewsCount enum right?Bethune
@JTAppleCalendarforiOSSwift That would actually be slightly more complicated, as what you're describing is not just an array of Int, but an array of JSON objects that each have an Int value for a given key – so what you'd need to do is iterate through the unkeyed container and get all the nested keyed containers, decoding an Int for each one (and then appending those to your array), e.g gist.github.com/hamishknight/9b5c202fe6d8289ee2cb9403876a1b41Chitchat
M
13
  1. Copy the json file to https://app.quicktype.io
  2. Select Swift (if you use Swift 5, check the compatibility switch for Swift 5)
  3. Use the following code to decode the file
  4. Voila!
let file = "data.json"

guard let url = Bundle.main.url(forResource: "data", withExtension: "json") else{
    fatalError("Failed to locate \(file) in bundle.")
}

guard let data = try? Data(contentsOf: url) else{
    fatalError("Failed to locate \(file) in bundle.")
}

let yourObject = try? JSONDecoder().decode(YourModel.self, from: data)
Midshipmite answered 31/1, 2020 at 15:32 Comment(1)
Worked for me, thank you. That site is gold. For viewers, if decoding a json string variable jsonStr, you can use this instead of the two guard lets above: guard let jsonStrData: Data? = jsonStr.data(using: .utf8)! else { print("error") } then convert jsonStrData to your struct as described above on the let yourObject lineLangmuir
R
9

Many good answers have already been posted, but there is a simpler method not described yet IMO.

When the JSON field names are written using snake_case_notation you can still use the camelCaseNotation in your Swift file.

You just need to set

decoder.keyDecodingStrategy = .convertFromSnakeCase

After this ☝️ line Swift will automatically match all the snake_case fields from the JSON to the camelCase fields in the Swift model.

E.g.

user_name` -> userName
reviews_count -> `reviewsCount
...

Here's the full code

1. Writing the Model

struct Response: Codable {

    let id: Int
    let user: User
    let reviewsCount: [ReviewCount]

    struct User: Codable {
        let userName: String

        struct RealInfo: Codable {
            let fullName: String
        }
    }

    struct ReviewCount: Codable {
        let count: Int
    }
}

2. Setting the Decoder

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

3. Decoding

do {
    let response = try? decoder.decode(Response.self, from: data)
    print(response)
} catch {
    debugPrint(error)
}
Ringside answered 13/5, 2019 at 9:27 Comment(1)
This doesn't address the original question how to deal with different levels of nesting.Finial
K
4

🛑 You don't need much like those giant answers!

You don't need to explain yourself. Almost everything can done automatically by Swift.

✅ Use a simple extension for your map:

extension Body {
    var username: String { user.userName }
    var fullName: String { user.realInfo.fullName }
    var reviewCount: Int { reviewsCount.first?.count ?? 0 }
}

You just need to define nested objectes "nestedly" in the way that API does:

struct Body: Codable {
    let id: Int
    private let user: User
    private let reviewsCount: [ReviewCount]

    struct User: Codable {
        let userName: String
        let realInfo: RealInfo

        struct RealInfo: Codable {
            let fullName: String
        }
    }

    struct ReviewCount: Codable {
        let count: Int
    }
}

and decode it with a decoder with the correct keyDecodingStrategy :

let jsonDecoder: JSONDecoder = {
    var decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase // 👈 this will take care of the key namings
    return decoder
}()
Keith answered 6/5, 2023 at 13:22 Comment(0)
M
0

Also you can use library KeyedCodable I prepared. It will require less code. Let me know what you think about it.

struct ServerResponse: Decodable, Keyedable {
  var id: String!
  var username: String!
  var fullName: String!
  var reviewCount: Int!

  private struct ReviewsCount: Codable {
    var count: Int
  }

  mutating func map(map: KeyMap) throws {
    var id: Int!
    try id <<- map["id"]
    self.id = String(id)

    try username <<- map["user.user_name"]
    try fullName <<- map["user.real_info.full_name"]

    var reviewCount: [ReviewsCount]!
    try reviewCount <<- map["reviews_count"]
    self.reviewCount = reviewCount[0].count
  }

  init(from decoder: Decoder) throws {
    try KeyedDecoder(with: decoder).decode(to: &self)
  }
}
Mirellamirelle answered 27/5, 2018 at 20:13 Comment(0)
G
0

An explanatory answer:

The issue at hand is:

"How to decode a nested JSON struct with Swift..."

As of writing, Swift does not have this:

struct Person: Codable {
    let height: String
    let hair: String
    let name: {
                let first: String
                let last: String
              }
}

You simply type this:

struct Person: Codable {
    let height: String
    let hair: String
    let name: Name
}
struct Name: Codable {
    let first: String
    let last: String
}

That's the whole thing.

Gigahertz answered 13/9, 2023 at 10:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.