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

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



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.




5 Answers
5



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 / struct
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.


reviews_count





Ok. this approach looks very clean. For my case, i think i'll use it
– iOS Calendar View OnMyProfile
Jun 14 '17 at 23:41





Yeah I definitely overthought this – @JTAppleCalendarforiOSSwift you should accept it, as it's a better solution.
– Hamish
Jun 15 '17 at 7:04





@Hamish ok. i switched it, but your answer was extremely detailed. I learned a lot from it.
– iOS Calendar View OnMyProfile
Jun 15 '17 at 14:09





Very clean solution, thank you!
– appsunited
Oct 26 '17 at 9:50





I'm curious to know how one can implement Encodable for the ServerResponse structure following the same approach. Is it even possible?
– nayem
May 25 at 6:12


Encodable


ServerResponse



In order to solve your problem, you can split your RawServerResponse implementation into several logic parts.


RawServerResponse


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



id


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


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)

/* ... */




fullName


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)

/* ... */




reviewCount


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




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




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
*/





Very dedicated answer.
– Hexfire
Sep 19 '17 at 7:50





Instead of struct you used enum with keys. which is much more elegant 👍
– Jack
Nov 17 '17 at 12:10


struct


enum





A huge thank-you for putting the time to document this so well. After scouring so much documentation on Decodable and parsing JSON, your answer really cleared up many questions I had.
– Marcy
Jun 9 at 5:55





This should be the accepted answer for me.
– A_C
Jul 12 at 13:41



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:


CodingKeys


// 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).


CodingKey



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:


container(keyedBy:)


nestedContainer(keyedBy:forKey:)


nestedUnkeyedContainer(forKey:)


nestedContainer(keyedBy:)


nestedUnkeyedContainer()



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)



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:


reviewCount


[Int]


"count"


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


"count"


count


isAtEnd



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)
)







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 ?
– iOS Calendar View OnMyProfile
Jun 14 '17 at 16:37



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





@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".
– Hamish
Jun 14 '17 at 16:41



CodingKeys


CodingKeys.User


"user_name": "Tester", "real_info": "full_name": "Jon Doe"


"user_name"


"real_info"





Thanks. Very clear response. Im still looking through it to understand it fully. But it works.
– iOS Calendar View OnMyProfile
Jun 14 '17 at 16:53





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?
– iOS Calendar View OnMyProfile
Jun 14 '17 at 17:13



reviews_count


var reviewCount: Int


var reviewCount: [Int]


ReviewsCount





@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/9b5c202fe6d8289ee2cb9403876a1b41
– Hamish
Jun 14 '17 at 17:18



Int


Int


Int



These guys already answered my question, but I just thought i'd post this link here which makes this a whole lot easier -> https://app.quicktype.io/#l=swift



Simply post your JSON response on the left hand pane, and watch your Models get generated on the right. This can help when youre just starting of.





Great resource - I’m just starting to learn this.
– Chris
May 27 at 22:32



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)




Thanks for contributing an answer to Stack Overflow!



But avoid



To learn more, see our tips on writing great answers.



Some of your past answers have not been well-received, and you're in danger of being blocked from answering.



Please pay close attention to the following guidance:



But avoid



To learn more, see our tips on writing great answers.



Required, but never shown



Required, but never shown






By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Popular posts from this blog

𛂒𛀶,𛀽𛀑𛂀𛃧𛂓𛀙𛃆𛃑𛃷𛂟𛁡𛀢𛀟𛁤𛂽𛁕𛁪𛂟𛂯,𛁞𛂧𛀴𛁄𛁠𛁼𛂿𛀤 𛂘,𛁺𛂾𛃭𛃭𛃵𛀺,𛂣𛃍𛂖𛃶 𛀸𛃀𛂖𛁶𛁏𛁚 𛂢𛂞 𛁰𛂆𛀔,𛁸𛀽𛁓𛃋𛂇𛃧𛀧𛃣𛂐𛃇,𛂂𛃻𛃲𛁬𛃞𛀧𛃃𛀅 𛂭𛁠𛁡𛃇𛀷𛃓𛁥,𛁙𛁘𛁞𛃸𛁸𛃣𛁜,𛂛,𛃿,𛁯𛂘𛂌𛃛𛁱𛃌𛂈𛂇 𛁊𛃲,𛀕𛃴𛀜 𛀶𛂆𛀶𛃟𛂉𛀣,𛂐𛁞𛁾 𛁷𛂑𛁳𛂯𛀬𛃅,𛃶𛁼

Edmonton

Crossroads (UK TV series)