Swift 4 JSON Codable ids as keys - json

I would like to use Swift 4's codable feature with json but some of the keys do not have a set name. Rather there is an array and they are ids like
{
"status": "ok",
"messages": {
"generalMessages": [],
"recordMessages": []
},
"foundRows": 2515989,
"data": {
"181": {
"animalID": "181",
"animalName": "Sophie",
"animalBreed": "Domestic Short Hair / Domestic Short Hair / Mixed (short coat)",
"animalGeneralAge": "Adult",
"animalSex": "Female",
"animalPrimaryBreed": "Domestic Short Hair",
"animalUpdatedDate": "6/26/2015 2:00 PM",
"animalOrgID": "12",
"animalLocationDistance": ""
where you see the 181 ids. Does anyone know how to handle the 181 so I can specify it as a key? The number can be any number and is different for each one.
Would like something like this
struct messages: Codable {
var generalMessages: [String]
var recordMessages: [String]
}
struct data: Codable {
var
}
struct Cat: Codable {
var Status: String
var messages: messages
var foundRows: Int
//var 181: [data] //What do I place here
}
Thanks in advance.

Please check :
struct ResponseData: Codable {
struct Inner : Codable {
var animalID : String
var animalName : String
private enum CodingKeys : String, CodingKey {
case animalID = "animalID"
case animalName = "animalName"
}
}
var Status: String
var foundRows: Int
var data : [String: Inner]
private enum CodingKeys: String, CodingKey {
case Status = "status"
case foundRows = "foundRows"
case data = "data"
}
}
let json = """
{
"status": "ok",
"messages": {
"generalMessages": ["dsfsdf"],
"recordMessages": ["sdfsdf"]
},
"foundRows": 2515989,
"data": {
"181": {
"animalID": "181",
"animalName": "Sophie"
},
"182": {
"animalID": "182",
"animalName": "Sophie"
}
}
}
"""
let data = json.data(using: .utf8)!
let decoder = JSONDecoder()
do {
let jsonData = try decoder.decode(ResponseData.self, from: data)
for (key, value) in jsonData.data {
print(key)
print(value.animalID)
print(value.animalName)
}
}
catch {
print("error:\(error)")
}

I don't think you can declare a number as a variable name. From Apple's doc:
Constant and variable names can’t contain whitespace characters,
mathematical symbols, arrows, private-use (or invalid) Unicode code
points, or line- and box-drawing characters. Nor can they begin with a
number, although numbers may be included elsewhere within the name.
A proper way is to have a property capturing your key.
struct Cat: Codable {
var Status: String
var messages: messages
var foundRows: Int
var key: Int // your key, e.g., 181
}

I would suggest something like this:-
struct ResponseData: Codable {
struct AnimalData: Codable {
var animalId: String
var animalName: String
private enum CodingKeys: String, CodingKey {
case animalId = "animalID"
case animalName = "animalName"
}
}
var status: String
var foundRows: Int
var data: [AnimalData]
private enum CodingKeys: String, CodingKey {
case status = "status"
case foundRows = "foundRows"
case data = "data"
}
struct AnimalIdKey: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int? { return nil }
init?(intValue: Int) { return nil }
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let animalsData = try container.nestedContainer(keyedBy: AnimalIdKey.self, forKey: .data)
self.foundRows = try container.decode(Int.self, forKey: .foundRows)
self.status = try container.decode(String.self, forKey: .status)
self.data = []
for key in animalsData.allKeys {
print(key)
let animalData = try animalsData.decode(AnimalData.self, forKey: key)
self.data.append(animalData)
}
}
}
let string = "{\"status\":\"ok\",\"messages\":{\"generalMessages\":[],\"recordMessages\":[]},\"foundRows\":2515989,\"data\":{\"181\":{\"animalID\":\"181\",\"animalName\":\"Sophie\"}}}"
let jsonData = string.data(using: .utf8)!
let decoder = JSONDecoder()
let parsedData = try? decoder.decode(ResponseData.self, from: jsonData)
This way your decoder initializer itself handles the keys.

Related

Decode JSON with variables in Swift

I am trying to decode this type of JSON-Data in Swift
{"Total ingredients":[{"PE-LLD":"54.4 %"},{"PE-HD":"41.1 %"},{"TiO2":"4.5 %"}]}
The name and number of ingredients is variable. Therefore I am only able to decode it in this type of structure:
struct Product: Codable {
var total_ingredients: [[String: String]]?
private enum CodingKeys : String, CodingKey {
case total_ingredients = "Total ingredients"
}
}
But I would like to be able to decode it in either one dictionary: var total_ingredients: [String: String]? or my preferred choice in an array of objects: var total_ingredients: [Ingredient]?
struct Ingredient: Codable {
var name: String
var percentage: String
}
I already tried to solve my problem with an extension but it isn't working and I don't think that's the correct approach:
extension Ingredient {
init(_ ingredient: [String: String]) {
var key: String = ""
var value: String = ""
for data in ingredient {
key = data.key
value = data.value
}
self = .init(name: key, percentage: value)
}
}
Thanks in advance :)
You have to implement init(from decoder and map the array of dictionaries to Ingredient instances
struct Product: Decodable {
let totalIngredients: [Ingredient]
private enum CodingKeys : String, CodingKey { case totalIngredients = "Total ingredients" }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let ingredientData = try container.decode([[String:String]].self, forKey: .totalIngredients)
totalIngredients = ingredientData.compactMap({ dict -> Ingredient? in
guard let key = dict.keys.first, let value = dict[key] else { return nil }
return Ingredient(name: key, percentage: value)
})
}
}
struct Ingredient {
let name, percentage: String
}
let jsonString = """
{"Total ingredients":[{"PE-LLD":"54.4 %"},{"PE-HD":"41.1 %"},{"TiO2":"4.5 %"}]}
"""
let data = Data(jsonString.utf8)
do {
let result = try JSONDecoder().decode(Product.self, from: data)
print(result)
} catch {
print(error)
}
The extension is not needed.

Parsing unknown number of items in SWIFT / SWIFT UI

im working on a project in SwiftUI and I am new to Parsing JSON.
I have the following JSON file:
{
"index": "001",
"name": "Joe",
"courses": {
"course_1": "English",
"course_2": "CS",
"course_3": "Maths"
},
"count": 3
}
My problem is that not every student has the same number of courses. How could I fetch courses without knowing how many there are ?
My struct:
struct Student: Decodable {
var index: String
var name: String
var courses: [Course]
var count: Int
struct Course {
}
}
You can also model the struct Student like so,
struct Student: Decodable {
var index: String
var name: String
var courses: [String:String]
var count: Int
}
In the above code, I've used [String:String] type for courses instead of [Course].
You can do some custom decoding:
struct Student : Decodable {
let index: String
let name: String
let courses: [String]
enum CodingKeys: CodingKey {
case index
case name
case count
case courses
}
struct CourseCodingKeys: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int?
init?(intValue: Int) {
// this creates a key such s "course_1" using an integer
self.stringValue = "course_\(intValue)"
}
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
// decode the simple properties
index = try container.decode(String.self, forKey: .index)
name = try container.decode(String.self, forKey: .name)
// get the count
let courseCount = try container.decode(Int.self, forKey: .count)
// get the object that has the courses
let nestedContainer = try container.nestedContainer(keyedBy: CourseCodingKeys.self, forKey: .courses)
if courseCount == 0 {
courses = []
} else {
// for 1 to courseCount, decode a string for that key
courses = try (1...courseCount)
.map { try nestedContainer.decode(String.self, forKey: CourseCodingKeys.init(intValue: $0)!) }
}
}
}
Usage:
// ["English", "CS", "Maths"]
try jsonDecoder.decode(Student.self, from: data).courses

Swift, How to Parse/Decode the JSON using Decodable and Codable, When key are unknow/dynamic

Below is my JSON, and I am not able to decode(using CodingKeys)
The data within the regions key is a Dictionary ("IN-WB", "IN-DL" & so on....), as the keys are dynamic, it can be changed more or less.
Please help me parsing the same using Decodable and Codable.
All the data should be within the single model.
{
"provider_code": "AIIN",
"name": "Jio India",
"regions": [
{
"IN-WB": "West Bengal"
},
{
"IN-DL": "Delhi NCR"
},
{
"IN-TN": "Tamil Nadu"
},
{
"IN": "India"
}
]
}
Just use a Dictionary for the regions.
struct Locations: Codable {
let providerCode: String
let name: String
let regions: [[String: String]]
enum CodingKeys: String, CodingKey {
case providerCode = "provider_code"
case name, regions
}
}
You cannot create a specific model for the regions as you wont know the property names
One of possible approach, without using dictionary. But still we have to found key at first )
I like this style as we can use Regions from beginning.
// example data.
let string = "{\"provider_code\":\"AIIN\",\"name\":\"Jio India\",\"regions\":[{\"IN-WB\":\"West Bengal\"},{\"IN-DL\":\"Delhi NCR\"},{\"IN-TN\":\"Tamil Nadu\"},{\"IN\":\"India\"}]}"
let data = string.data(using: .utf8)!
// little helper
struct DynamicGlobalKey: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int? { return nil }
init?(intValue: Int) { return nil }
}
// model
struct Location: Decodable {
let providerCode: String
let name: String
let regions: [Region]
}
extension Location {
struct Region: Decodable {
let key: String
let name: String
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: DynamicGlobalKey.self)
key = container.allKeys.first!.stringValue
name = try container.decode(String.self, forKey: container.allKeys.first!)
}
}
}
// example of decoding.
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let location = try decoder.decode(Location.self, from: data)

How to extract data from nested JSON with Swift 4 with dynamic keys

I have a JSON data structure using unique keys that are created on upload. I can read all of it if I read each dictionary item line by line. However, I'm trying to modify my code to use the Swift 4 codable properties.
Doing the Ray Wenderlich tutorial and reading the Ultimate Guide to JSON Parsing with Swift unfortunately did not propel me to genius status.
The JSON looks like this simple example:
NOTE that keys, like "123", "456", "case1", "case2", "u1", "u2" are not known at run time.
let json = """
{
"things" : {
"123" : {
"name" : "Party",
"owner" : "Bob",
"isActive" : true,
"cases" : {
"case1" : {
"no" : 1
},
"case2" : {
"no" : 2
}
}
},
"456" : {
"name" : "Bus",
"owner" : "Joe",
"isActive" : true
}
},
"users" : {
"u1" : {
"name" : "Summer"
},
"u2" : {
"name" : "Daffy"
}
}
}
"""
Following this SO question on flattening JSON, I was able to create a decoder for most of my data, but not for the nested dictionaries (in the example, cases is acting like a nested dictionary). I am sure that I am missing something simple.
If I attempt to include the commented out portion, the playground will not run, no error is given.
struct Thing: Decodable {
let id: String
let isActive: Bool
let name: String
let owner: String
//var cases = [Case]()
init(id: String, isActive: Bool, name: String, owner: String){//}, cases: [Case]?) {
self.id = id
self.isActive = isActive
self.name = name
self.owner = owner
//self.cases = cases ?? [Case(id: "none", caseNumber: 0)]
}
}
struct User: Decodable {
let id: String
let name: String
}
struct Case: Decodable {
let id: String
let caseNumber: Int
}
struct ResponseData: Decodable {
var things = [Thing]()
var users = [User]()
enum CodingKeys: String, CodingKey {
case trips
case users
}
private struct PhantomKeys: CodingKey {
var intValue: Int?
var stringValue: String
init?(intValue: Int) { self.intValue = intValue; self.stringValue = "\(intValue)" }
init?(stringValue: String) { self.stringValue = stringValue }
}
private enum ThingKeys: String, CodingKey {
case isActive, name, owner, cases
}
private enum UserKeys: String, CodingKey {
case name
}
private enum CaseKeys: String, CodingKey {
case id
case caseNumber = "no"
}
init(from decoder: Decoder) throws {
let outer = try decoder.container(keyedBy: CodingKeys.self)
let thingcontainer = try outer.nestedContainer(keyedBy: PhantomKeys.self, forKey: .things)
for key in thingcontainer.allKeys {
let aux = try thingcontainer.nestedContainer(keyedBy: ThingKeys.self, forKey: key)
let name = try aux.decode(String.self, forKey: .name)
let owner = try aux.decode(String.self, forKey: .owner)
let isActive = try aux.decode(Bool.self, forKey: .isActive)
// let c = try aux.nestedContainer(keyedBy: CaseKeys.self, forKey: .cases)
// var cases = [Case]()
// for ckey in c.allKeys {
// let caseNumber = try c.decode(Int.self, forKey: .caseNumber)
// let thiscase = Case(id: ckey.stringValue, caseNumber: caseNumber)
// cases.append(thiscase)
// }
let thing = Thing(id: key.stringValue, isActive: isActive, name: name, owner: owner)//, cases: cases)
things.append(thing)
}
let usercontainer = try outer.nestedContainer(keyedBy: PhantomKeys.self, forKey: .users)
for key in usercontainer.allKeys {
let aux = try usercontainer.nestedContainer(keyedBy: UserKeys.self, forKey: key)
let name = try aux.decode(String.self, forKey: .name)
let user = User(id: key.stringValue,name: name)
users.append(user)
}
}
}
It works for the things and users, but I have to ignore the cases. See the output of print in comments//.
let data = json.data(using: .utf8)!
let things = try JSONDecoder().decode(ResponseData.self, from: data).things
print(things[0])
//Thing(id: "456", isActive: true, name: "Bus", owner: "Joe")
let users = try JSONDecoder().decode(ResponseData.self, from: data).users
print(users[0])
//User(id: "u1", name: "Summer")
I have tried to use the guidance from this SO question on decoding that seems much cleaner to me, but I have not successfully implemented it.
This code is also a GIST
My question is twofold:
How can I get the Case data as a nested array in my Thing?
Can you
suggest a cleaner/shorter way to code this? It feels like I'm
repeating things, but I have seen this kind of wrapper structure in
several examples for JSON encoding/decoding.
You can try something like this:
let data = jsonData.data(using: .utf8)
let json = try JSONSerialization.jsonObject(with: data!) as! [String:Any]
let things = json["things"] as! [String:Any]
for (thing_key, thing_value) in things as [String:Any] {
let thing = thing_value as! [String:Any]
if let cases = thing["cases"] as? [String:Any]{
for (case_key, case_value) in cases {
print(case_key)
print(case_value)
}
}
}
EDIT
I initially missunderstood your question , here is your code improved to obtain the cases. It was a quick job so might not be optimal, but you get the idea:
struct Thing: Decodable {
let id: String
let isActive: Bool
let name: String
let owner: String
var cases: [Case]?
init(id: String, isActive: Bool, name: String, owner: String , cases: [Case]?) {
self.id = id
self.isActive = isActive
self.name = name
self.owner = owner
self.cases = cases
}
}
struct User: Decodable {
let id: String
let name: String
}
struct Case: Decodable {
let id: String
let caseNumber: Int
}
struct ResponseData: Decodable {
var things = [Thing]()
var users = [User]()
enum CodingKeys: String, CodingKey {
case things
case users
case cases
}
private struct PhantomKeys: CodingKey {
var intValue: Int?
var stringValue: String
init?(intValue: Int) { self.intValue = intValue; self.stringValue = "\(intValue)" }
init?(stringValue: String) { self.stringValue = stringValue }
}
private enum ThingKeys: String, CodingKey {
case isActive, name, owner, cases
}
private enum UserKeys: String, CodingKey {
case name
}
private enum CaseKeys: String, CodingKey {
case no
}
init(from decoder: Decoder) throws {
let outer = try decoder.container(keyedBy: CodingKeys.self)
let thingcontainer = try outer.nestedContainer(keyedBy: PhantomKeys.self, forKey: .things)
for key in thingcontainer.allKeys {
let aux = try thingcontainer.nestedContainer(keyedBy: ThingKeys.self, forKey: key)
let name = try aux.decode(String.self, forKey: .name)
let owner = try aux.decode(String.self, forKey: .owner)
let isActive = try aux.decode(Bool.self, forKey: .isActive)
var cases:[Case]? = []
do{
let casescontainer = try aux.nestedContainer(keyedBy: PhantomKeys.self, forKey: .cases)
for case_key in casescontainer.allKeys{
let caseaux = try casescontainer.nestedContainer(keyedBy: CaseKeys.self, forKey: case_key)
let no = try caseaux.decode(Int.self, forKey: .no)
let thingCase = Case(id:case_key.stringValue, caseNumber: no)
cases?.append(thingCase)
}
}catch{ }
let thing = Thing(id: key.stringValue, isActive: isActive, name: name, owner: owner , cases: cases)
things.append(thing)
}
let usercontainer = try outer.nestedContainer(keyedBy: PhantomKeys.self, forKey: .users)
for key in usercontainer.allKeys {
let aux = try usercontainer.nestedContainer(keyedBy: UserKeys.self, forKey: key)
let name = try aux.decode(String.self, forKey: .name)
let user = User(id: key.stringValue,name: name)
users.append(user)
}
}
}
This produce this output:
let data = json.data(using: .utf8)!
let things = try JSONDecoder().decode(ResponseData.self, from: data).things
print("-----")
for thing in things{
print(thing)
}
print("---")
let users = try JSONDecoder().decode(ResponseData.self, from: data).users
for user in users{
print(user)
}
-----
Thing(id: "456", isActive: true, name: "Bus", owner: "Joe", cases: Optional([]))
Thing(id: "123", isActive: true, name: "Party", owner: "Bob", cases: Optional([__lldb_expr_283.Case(id: "case1", caseNumber: 1), __lldb_expr_283.Case(id: "case2", caseNumber: 2)]))
---
User(id: "u1", name: "Summer")
User(id: "u2", name: "Daffy")
You can get keys from your current json as
jq -r 'keys[]'
After that query in the loop by each key retrieved

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.
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.
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
*/
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:
nestedContainer(keyedBy:forKey:) to get a nested object from an object for a given key
nestedUnkeyedContainer(forKey:) to get a nested array from an object for a given key
nestedContainer(keyedBy:) to get the next nested object from an array
nestedUnkeyedContainer() to get the next nested array from an array
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)
)
}
}
}
Copy the json file to https://app.quicktype.io
Select Swift (if you use Swift 5, check the compatibility switch for Swift 5)
Use the following code to decode the file
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)
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)
}
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)
}
}