Swift - How to have multiple decoder method for a single model - json

struct Classroom: Codable {
let teacher: String
let id: Int
let status: Bool
init(from decoder: Decoder) throws {
...
...
}
}
Now I need a way to create Classroom instance with a simple String
{ "classroom": "Test" }//received from API
let classRoom = ClassRoom(teacher: "Test", id: 0, status: true)
Now I need to add a secondary decoder method which can create this classroom instance using the "classroom": "Test" data. The "Test" value should be used as value for "teacher" and other properties should contain default values.
I know I can decode the String value and create a new initializer. Is there a way to directly decode String to this model object?

if i understand well, i assume you have a bad json format like below
[
{
"teacher":"test",
"id":5,
"status":true
},
{
"classroom":"Test"
}
]
And you want to decode both objects, you can do the following
let data = """
[
{
"teacher": "test",
"id": 5,
"status": true
},
{
"classroom": "Test"
}
]
""".data(using: .utf8)!
struct Classroom: Codable {
let teacher: String
let id: Int
let status: Bool
private enum CodingKeys: CodingKey {
case teacher, id, status
}
private enum SecCodingKeys: CodingKey {
case classroom
}
init(from decoder: Decoder) throws {
let value = try decoder.container(keyedBy: CodingKeys.self)
let secValue = try decoder.container(keyedBy: SecCodingKeys.self)
let teacher_1 = try value.decodeIfPresent(String.self, forKey: .teacher)
let teacher_2 = try secValue.decodeIfPresent(String.self, forKey: .classroom)
teacher = teacher_1 ?? teacher_2 ?? ""
id = try value.decodeIfPresent(Int.self, forKey: .id) ?? 0
status = try value.decodeIfPresent(Bool.self, forKey: .status) ?? false
}
}
do {
let rooms = try JSONDecoder().decode([Classroom].self, from: data)
print(rooms.map(\.teacher))
} catch {
print(error)
}
and the result,
["test", "Test"]

Decode the second, nested case, as another type
struct SimpleClassroom: Decodable {
let classroom: String
}
and then have a computed property for mapping to the original type with default values
extension SimpleClassroom {
var classroomValue: Classroom {
Classroom(teacher: classroom, id: 0, status: false)
}
}

If "Test" is a valid description of classroom, and you want to go ahead and create the classroom, then you have a number of options.
If you know classrooms from a given API endpoint will always be in this string format, you can use the decoder's context dictionary to tell it up front which strategy to use to decode the classroom. If sometimes a classroom is a properly formed dictionary, and sometimes it's just a string, and you want to proceed either way, then you have to handle that case in the init(from:).
Either way you're looking at a custom init method. The second case, where you handle both types, would look like this:
init(from decoder: Decoder) throws {
// Do we have a single-value container?
do {
let container = try decoder.singleValueContainer()
let string = try container.decode(String.self)
self.teacher = string
self.id = 0
self.status = true
return
} catch {
// OK, it was a dictionary
}
let container = try decoder.container(keyedBy: CodingKeys.self)
self.teacher = try container.decode(String.self, forKey: .teacher)
self.id = try container.decode(Int.self, forKey: .id)
self.status = try container.decode(Bool.self, forKey: .status)
}
Given this made-up, horrible JSON:
[
"Test",
{ "teacher": "Mr Chips", "id": 0, "status": true }
]
let rooms = try JSONDecoder().decode([Classroom].self, from: data)
Gives you two valid Classroom types in an array.

Related

Parsing complex JSON where data and "column headers" are in separate arrays

I have the following JSON data I get from an API:
{"datatable":
{"data" : [
["John", "Doe", "1990-01-01", "Chicago"],
["Jane", "Doe", "2000-01-01", "San Diego"]
],
"columns": [
{ "name": "First", "type": "String" },
{ "name": "Last", "type": "String" },
{ "name": "Birthday", "type": "Date" },
{ "name": "City", "type": "String" }
]}
}
A later query could result the following:
{"datatable":
{"data" : [
["Chicago", "Doe", "John", "1990-01-01"],
["San Diego", "Doe", "Jane", "2000-01-01"]
],
"columns": [
{ "name": "City", "type": "String" },
{ "name": "Last", "type": "String" },
{ "name": "First", "type": "String" },
{ "name": "Birthday", "type": "Date" }
]
}
}
The order of the colums seems to be fluid.
I initially wanted to decode the JSON with JSONDecoder, but for that I need the data array to be a dictionary and not an array.
The only other method I could think of was to convert the result to a dictionary with something like:
extension String {
func convertToDictionary() -> [String: Any]? {
if let data = data(using: .utf8) {
return try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
}
return nil
}
}
This will cause me however to have a lot of nested if let statements like if let x = dictOfStr["datatable"] as? [String: Any] { ... }.
Not to mention the subsequent looping through the columns array to organize the data.
Is there a better solution?
Thanks
You could still use JSONDecoder, but you'd need to manually decode the data array.
To do that, you'd need to read the columns array, and then decode the data array using the ordering that you got from the columns array.
This is actually a nice use case for KeyPaths. You can create a mapping of columns to object properties, and this helps avoid a large switch statement.
So here's the setup:
struct DataRow {
var first, last, city: String?
var birthday: Date?
}
struct DataTable: Decodable {
var data: [DataRow] = []
// coding key for root level
private enum RootKeys: CodingKey { case datatable }
// coding key for columns and data
private enum CodingKeys: CodingKey { case data, columns }
// mapping of json fields to properties
private let fields: [String: PartialKeyPath<DataRow>] = [
"First": \DataRow.first,
"Last": \DataRow.last,
"City": \DataRow.city,
"Birthday": \DataRow.birthday ]
// I'm actually ignoring here the type property in JSON
private struct Column: Decodable { let name: String }
// init ...
}
Now the init function:
init(from decoder: Decoder) throws {
let root = try decoder.container(keyedBy: RootKeys.self)
let inner = try root.nestedContainer(keyedBy: CodingKeys.self, forKey: .datatable)
let columns = try inner.decode([Column].self, forKey: .columns)
// for data, there's more work to do
var data = try inner.nestedUnkeyedContainer(forKey: .data)
// for each data row
while !data.isAtEnd {
let values = try data.decode([String].self)
var dataRow = DataRow()
// decode each property
for idx in 0..<values.count {
let keyPath = fields[columns[idx].name]
let value = values[idx]
// now need to decode a string value into the correct type
switch keyPath {
case let kp as WritableKeyPath<DataRow, String?>:
dataRow[keyPath: kp] = value
case let kp as WritableKeyPath<DataRow, Date?>:
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "YYYY-MM-DD"
dataRow[keyPath: kp] = dateFormatter.date(from: value)
default: break
}
}
self.data.append(dataRow)
}
}
To use this, you'd use the normal JSONDecode way:
let jsonDecoder = JSONDecoder()
let dataTable = try jsonDecoder.decode(DataTable.self, from: jsonData)
print(dataTable.data[0].first) // prints John
print(dataTable.data[0].birthday) // prints 1990-01-01 05:00:00 +0000
EDIT
The code above assumes that all the values in a JSON array are strings and tries to do decode([String].self). If you can't make that assumption, you could decode the values to their underlying primitive types supported by JSON (number, string, bool, or null). It would look something like this:
enum JSONVal: Decodable {
case string(String), number(Double), bool(Bool), null, unknown
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let v = try? container.decode(String.self) {
self = .string(v)
} else if let v = try? container.decode(Double.self) {
self = .number(v)
} else if ...
// and so on, for null and bool
}
}
Then, in the code above, decode the array into these values:
let values = try data.decode([JSONValue].self)
Later when you need to use the value, you can examine the underlying value and decide what to do:
case let kp as WritableKeyPath<DataRow, Int?>:
switch value {
case number(let v):
// e.g. round the number and cast to Int
dataRow[keyPath: kp] = Int(v.rounded())
case string(let v):
// e.g. attempt to convert string to Int
dataRow[keyPath: kp] = Int((Double(str) ?? 0.0).rounded())
default: break
}
It appears that the data and columns values gets encoded in the same order so using that we can create a dictionary for column and array of values where each array is in the same order.
struct Root: Codable {
let datatable: Datatable
}
struct Datatable: Codable {
let data: [[String]]
let columns: [Column]
var columnValues: [Column: [String]]
enum CodingKeys: String, CodingKey {
case data, columns
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
data = try container.decode([[String]].self, forKey: .data)
columns = try container.decode([Column].self, forKey: .columns)
columnValues = [:]
data.forEach {
for i in 0..<$0.count {
columnValues[columns[i], default: []].append($0[i])
}
}
}
}
struct Column: Codable, Hashable {
let name: String
let type: String
}
Next step would be to introduce a struct for the data
The way I would do it is to create two model objects and have them both conform to the Codable protocol like so:
struct Datatable: Codable {
let data: [[String]]
let columns: [[String: String]]
}
struct JSONResponseType: Codable {
let datatable: Datatable
}
Then in your network call I'd decode the json response using JSONDecoder():
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
guard let decodedData = try? decoder.decode(JSONResponseType.self, from: data) else {
// handle decoding failure
return
}
// do stuff with decodedData ex:
let datatable = decodedData.datatable
...
data in this case is the result from the URLSessionTask.
Let me know if this works.
Maybe try to save the given input inside a list of user objects? This way however the JSON is structured you can add them in the list and handle them after anyway you like. Maybe an initial alphabetical ordering after name would also help with the display order of users.
Here is a sample I wrote, instead of logging the info you can add a new UserObject to the list with the currently printed information.
let databaseData = table["datatable"]["data"];
let databaseColumns = table["datatable"]["columns"];
for (let key in databaseData) {
console.log(databaseColumns[0]["name"] + " = " + databaseData[key][0]);
console.log(databaseColumns[1]["name"] + " = " + databaseData[key][1]);
console.log(databaseColumns[2]["name"] + " = " + databaseData[key][2]);
console.log(databaseColumns[3]["name"] + " = " + databaseData[key][3]);
}
The only thing I could think of is:
struct ComplexValue {
var value:String
var columnName:String
var type:String
}
struct ComplexJSON: Decodable, Encodable {
enum CodingKeys: String, CodingKey {
case data, columns
}
var data:[[String]]
var columns:[ColumnSpec]
var processed:[[ComplexValue]]
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
data = (try? container.decode([[String]].self, forKey: .data)) ?? []
columns = (try? container.decode([ColumnSpec].self, forKey: .columns)) ?? []
processed = []
for row in data {
var values = [ComplexValue]()
var i = 0
while i < columns.count {
var item = ComplexValue(value: row[i], columnName: columns[i].name, type: columns[i].type)
values.append(item)
i += 1
}
processed.append(values)
}
}
}
struct ColumnSpec: Decodable, Encodable {
enum CodingKeys: String, CodingKey {
case name, type
}
var name:String
var type:String
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = (try? container.decode(String.self, forKey: .name)) ?? ""
type = (try? container.decode(String.self, forKey: .type)) ?? ""
}
}
Now you would have the processed variable which would contain formatted version of your data. Well, formatted might not be the best word, given that structure is completely dynamic, but at least whenever you extract some specific cell you would know its value, type and its column name.
I don't think you can do anything more specific than this without extra details about your APIs.
Also, please note that I did this in Playground, so some tweaks might be needed to make the code work in production. Although I think the idea is clearly visible.
P.S. My implementation does not deal with "datatable". Should be straightforward to add, but I thought it would only increase the length of my answer without providing any benefits. After all, the challenge is inside that field :)

ignore null object in array when parse with Codable swift

i'm parsing this API with swift Codable
"total": 7,
"searchResult": [
null,
{
"name": "joe"
"family": "adam"
},
null,
{
"name": "martin"
"family": "lavrix"
},
{
"name": "sarah"
"family": "mia"
},
null,
{
"name": "ali"
"family": "abraham"
}
]
with this PaginationModel:
class PaginationModel<T: Codable>: Codable {
var total: Int?
var data: T?
enum CodingKeys: String, CodingKey {
case total
case data = "searchResult"
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.total = try container.decodeIfPresent(Int.self, forKey: .total)
self.data = try container.decodeIfPresent(T.self, forKey: .data)
}
}
and User Model:
struct User: Codable {
var name: String?
var family: String?
}
i call jsonDecoder like this to parse API json:
let responseObject = try JSONDecoder().decode(PaginationModel<[User?]>.self, from: json)
now my problem is null in searchResult Array. it parsed correctly and when i access to data in paginationModel i found null in array.
how can i ignore all null when parsing API, and result will be an array without any null
In the first place, I would advise to always consider PaginationModel to be composed from arrays. You don't have to pass [User] as the generic type, you can just pass User. Then the parser can use the knowledge that it parses arrays and handle null automatically:
class PaginationModel<T: Codable>: Codable {
var total: Int?
var data: [T]?
enum CodingKeys: String, CodingKey {
case total
case data = "searchResult"
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.total = try container.decodeIfPresent(Int.self, forKey: .total)
self.data = (try container.decodeIfPresent([T?].self, forKey: .data))?.compactMap { $0 }
}
}
You might want to remove optionals here and use some default values instead:
class PaginationModel<T: Codable>: Codable {
var total: Int = 0
var data: [T] = []
enum CodingKeys: String, CodingKey {
case total
case data = "searchResult"
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.total = (try container.decodeIfPresent(Int.self, forKey: .total)) ?? 0
self.data = ((try container.decodeIfPresent([T?].self, forKey: .data)) ?? []).compactMap { $0 }
}
}
Simple solution, filter data after decoding
let responseObject = try JSONDecoder().decode(PaginationModel<[User?]>.self, from: data)
responseObject.data = responseObject.data?.filter{$0 != nil}
You may add an array type check within decode :
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.total = try container.decodeIfPresent(Int.self, forKey: .total)
self.data = try container.decodeIfPresent(T.self, forKey: .data)
//add the following:
if let array = self.data as? Array<Any?> {
self.data = ( array.compactMap{$0} as? T)
}
}
Note, you can just define the decodable variable that may be null/nil as [Float?] (or whatever type), with the optional '?' inside the array brackets.

swift4 encoding decoding for model class of nested json parsing

I have a model class of swift which was created based on a nested json response, it follows like below
struct RootClass : Codable {
let details : String?
let itemCount : Int?
let list : [List]?
enum CodingKeys: String, CodingKey {
case details = "Details"
case itemCount = "ItemCount"
case list = "List"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
details = try values.decodeIfPresent(String.self, forKey: .details)
itemCount = try values.decodeIfPresent(Int.self, forKey: .itemCount)
list = try values.decodeIfPresent([List].self, forKey: .list)
}
}
struct List : Codable {
let companyID : Int?
let employeeCount : Int?
let employeeUser : EmployeeUser?
enum CodingKeys: String, CodingKey {
case companyID = "CompanyID"
case employeeCount = "EmployeeCount"
case employeeUser = "EmployeeUser"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
companyID = try values.decodeIfPresent(Int.self, forKey: .companyID)
employeeCount = try values.decodeIfPresent(Int.self, forKey: .employeeCount)
employeeUser = try EmployeeUser(from: decoder)
}
}
struct EmployeeUser : Codable {
let mobileNumber : String?
let name : String?
enum CodingKeys: String, CodingKey {
case mobileNumber = "MobileNumber"
case name = "Name"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
mobileNumber = try values.decodeIfPresent(String.self, forKey: .mobileNumber)
name = try values.decodeIfPresent(String.self, forKey: .name)
}
}
and my json response is
{
"Details": null,
"List": [
{
"CompanyID": 140,
"EmployeeUser": {
"Name": " raghu2",
"MobileNumber": "8718718710"
},
"EmployeeCount": 0
},
{
"CompanyID": 140,
"EmployeeUser": {
"Name": "new emp reg",
"MobileNumber": "1"
},
"EmployeeCount": 0
}
],
"ItemCount": 0
}
I am trying to parse it like
guard let data = data else { return }
do {
let decoder = JSONDecoder()
let gitData = try decoder.decode(RootClass.self, from: data)
print(gitData.itemCount ?? "")
print(gitData.list![0].employeeUser?.mobileNumber ?? "")
}
catch let err {
print("Err", err)
}
I am able to get the values of root class and list but I am getting nil values under employee user section.
Your code a few problems:
All your keys are optional. The vendor API will tell you what keys are always present and which one are optional. Follow that.
decodeIfPresent will silently fail if it cannot decode a key. When debugging your app, you want things to fail with a bang so you can fix the error before going to production.
You wrote way more code than needed. All those init(from decoder: ) functions are not needed. One one did cause your problem.
Your problem was caused by this line:
struct List : Codable {
init(from decoder: Decoder) throws {
...
employeeUser = try EmployeeUser(from: decoder)
}
}
You are asking Swift to decode to same JSON to a List and a EmployeeUser object. Obviously, that's not valid. But when you decode list inside RootClass, you call decodeIfPresent:
// In Rootclass
list = try values.decodeIfPresent([List].self, forKey: .list)
This call silently failed and you never knew what the problem was!
Solution
Change how you initialize employeeUser to this:
employeeUser = try values.decodeIfPresent(EmployeeUser.self, forKey: .employeeUser)
But the most elegant solution is to delete all those init(from decoder: ). The compiler will synthesize them for you.
And finally, fix those optionals!

parse JSON with swift via a model to object - decode int/string trouble

To get JSON from a website and turn it into an object is fairly simple using swift 4:
class func getJSON(completionHandler: #escaping (MyModel) -> Void)
{
let myUrl = "https://example.com/whatever"
if let myUrl = URL(string: myUrl)
{
URLSession.shared.dataTask(with: myUrl)
{ (data, response, err) in
if let data = data
{
do
{
let myData = try JSONDecoder().decode(MyModel.self, from: data)
completionHandler(myData)
}
catch let JSONerr
{
print("Error: \(JSONerr)")
}
}
return
}.resume()
}
}
MyModel is a data model:
struct MyModel
{
products: [MyProduct]
}
struct MyProduct
{
id: Int
...
I use this to GET from my WebService and it works well for most JSON structures.
However, I facing trouble with this complex JSON object. (By complex I mean too long to post here, so I hope you can figure-out such a pattern. Also the JSON object it has many nested arrays, etc.)
Eg.
{
"products" : [
{
"name" : "abc"
"colors" : [
{
"outside" : [
{
"main" : "blue"
}]
}]
"id" :
...
},
{
"name" : "xyzzy"
"colors" : [
{
"outside" : [
{
"main" : "blue"
}]
}]
"id" :
...
}]
}
(This may not be valid JSON - it is a simple extract of a larger part.)
The app crashes "...Expected to decode String but found a number
instead."!
So I change the model to use a 'String' in place of the
'Int'.
Again it crashes, saying "...Expected to decode Int but found
a string/data instead."!
So I change the model back, place an 'Int'
in place of the 'String'.
(The cycle repeats.)
It seems the value in question is sometimes an Int and sometimes a
String.
This NOT only happens with a certain key. I know of at least five other similar cases in this JSON.
So that means that I may get another error for another key, if a solution was only for that specific key. I would not be surprised to find many other cases as well.
QUESTION: How can I properly decode the JSON to my object, where the type of its elements can be either an Int or a String?
I want a solution that will either apply to all Model members or try convert a value to a String type if Int fails. Since I don't know which other keys will as fail.
You can use if lets to handle unpredictable values:
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: MemberKeys.self)
if let memberValue = try? container.decode([String].self, forKey: .member){
stringArrayMember = memberValue
}
else if let str = try? container.decode(String.self, forKey: .member){
stringMember = str
}
else if let int = try? container.decode(Int.self, forKey: .member){
intMember = int
}
}
Or if it's a specific case of String vs Int and you'd like the same variable to handle the values, then something like:
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: MemberKeys.self)
if let str = try? container.decode(String.self, forKey: .member){
stringMember = str
}
else if let int = try? container.decode(Int.self, forKey: .member){
stringMember = String(int)
}
}
Edit
Your MyProduct will now look like:
struct MyProduct: Decodable {
var id: String?
var someOtherProperty: String?
enum MemberKeys: String, CodingKey {
case id
case someOtherProperty
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: MemberKeys.self)
someOtherProperty = try? container.decode(String.self, forKey: .someOtherProperty)
// Problematic property which can be String/Int
if let str = try? container.decode(String.self, forKey: .id){
id = str
}
else if let int = try? container.decode(Int.self, forKey: .id){
id = String(int)
}
}
}
Hope this helps.
This wasn't the problem that the error message gave!
All I needed to do to fix the problem was to employ CodingKeys.
I was hoping to avoid this since the data structure (JSON) had lots of members. But this fixed the problem.
Now an example of my model:
struct Product
{
let name: String
let long_name_value: String
...
enum MemberKeys: String, CodingKey
{
case name
case longNameValue = "long_name_value"
...
}
}
I guess the reason is swift doesn't like snake case (eg. "long_name_value"), so I needed to convert it to camel case (eg."longNameValue"). Then the errors disappeared.

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