Swift 4 Decoding to different models from custom JSON objects - json

I have a websocket which generate different json objects. Objects could contain no any common fields
{
"type": "apple",
"kind": "fruit",
"eatable": true
}
{
"item": "key",
"active": true
}
{
"tool": "screwdriver",
"original": "toolBox",
"cross-head": true
}
I have a list of classes for them (they could contain some logic) so I need to parse it to map some those models with some hierarchical structure e.g.
Try to parse fruits if they fails try to parse keys if they fails try to parse toolbox. Sometimes I need to add some new classes to parse some objects and some new fields to existing classes.
How to organize picking class for parsing?
Update
I have no control on backend data so I cannot add any fields to JSON I have.
Objects come one at a time. I have separate class models for most of them. The issue is to choose the right class to map the JSON fields.

You can do it this way:
First you declare your types conforming to the Decodable protocole:
struct Fruit : Decodable {
let type : String
let kind : String
let eatable : Bool
}
struct Tool : Decodable {
let tool : String
let original : String
let crossHead : Bool
enum CodingKeys: String, CodingKey {
case tool = "tool"
case original = "original"
case crossHead = "cross-head"
}
}
Then you extend Decodable to "reverse" the use of the genericity:
extension Decodable {
static func decode(data : Data, decoder : JSONDecoder = JSONDecoder()) -> Self? {
return try? decoder.decode(Self.self, from: data)
}
}
You then extend JSONDecoder to try decodable types among the ones you want to test:
extension JSONDecoder {
func decode(possibleTypes : [Decodable.Type], from data: Data) -> Any? {
for type in possibleTypes {
if let value = type.decode(data: data, decoder: self) {
return value
}
}
return nil
}
}
And eventually you specify the types you want to try and decode:
let decodableTypes : [Decodable.Type] = [Fruit.self, Tool.self]
You can then use it to decode your JSON:
let jsonString = """
{
"tool": "screwdriver",
"original": "toolBox",
"cross-head": true
}
"""
let jsonData = jsonString.data(using: .utf8)!
let myUnknownObject = JSONDecoder().decode(possibleTypes: decodableTypes, from: jsonData)
And voilà!!!
Now you can add as much types as you want in your decodableTypes as long as they conform to the Decodable protocol.
It is not the best approach, because if you have many types it won't be optimal, but this way you don't need to add a discriminating field in your data.

Try finding the key you are looking for that model class if that key is not present in that object try another model class. This should make you determine which model class is suitable for the given object.
Use the unique key which is not present in any other model class
Example:
var array = NSArray(array: [[
"type": "apple",
"kind": "fruit",
"eatable": true
],
[
"item": "key",
"active": true
],
[
"tool": "screwdriver",
"original": "toolBox",
"cross-head": true
]])
for model in array as! [NSDictionary]
{
if(model.value(forKey: "type") != nil)
{
print("use Fruit Model Class")
}
else if(model.value(forKey: "item") != nil)
{
print("use second model class")
}
else
{
print("use third model class")
}
}

If all those fields are related or a union style, you may consider user Enum, which is also very easy to implement.
let data1 = """
[{
"type": "apple",
"kind": "fruit",
"eatable": true
},
{
"item": "key",
"active": true
},
{
"tool": "screwdriver",
"original": "toolBox",
"cross-head": true
}]
""".data(using: .utf8)!
struct JSONType : Decodable{
let type: String
let kind: String
let eatable : Bool
}
struct JSONItem : Decodable{
let item: String
let active : Bool
}
struct JSONTool : Decodable{
let tool: String
let original : String
let crosshead : Bool
enum CodingKeys: String, CodingKey {
case tool = "tool"
case original = "original"
case crosshead = "cross-head"
}
}
enum JSONData : Decodable{
case type(JSONType)
case item(JSONItem)
case tool(JSONTool)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do{ let temp = try container.decode(JSONType.self); self = .type(temp) ; return}
catch{do { let temp = try container.decode(JSONItem.self) ; self = .item(temp) ; return}
catch{ let temp = try container.decode(JSONTool.self) ; self = .tool(temp) ; return}}
try self.init(from: decoder)
}
func getValue()-> Any{
switch self {
case let .type(x): return x
case let .item(x): return x
case let .tool(x): return x
}
}
}
let result = try JSONDecoder().decode([JSONData].self, from: data1)
print(result[0].getValue())
print (result[1].getValue())
print (result[2].getValue())

Related

Swift & Codable: how to "bypass" some JSON levels?

I would like to use this Pokémon API to fetch some data and convert it into a Swift Pokemon struct.
Here is an extract of the response I get when fetching Pokemon #142:
{
"id": 142,
"name": "aerodactyl",
"types": [{
"type": {
"name": "rock",
"url": "https://pokeapi.co/api/v2/type/6/"
},
"slot": 1
},
{
"type": {
"name": "flying",
"url": "https://pokeapi.co/api/v2/type/3/"
},
"slot": 2
}
]
}
Here is the struct I wrote to convert this JSON into a Swift type:
struct Pokemon: Codable {
var id: Int
let name: String
var types: [PokemonType]?
}
struct PokemonType: Codable {
var type: PokemonTypeContent
}
struct PokemonTypeContent: Codable {
var name: PokemonTypeNameContent
}
enum PokemonTypeNameContent: String, Codable {
case flying = "flying"
case rock = "rock"
// ...
}
Now here is my problem: when I want to get the Pokemon types, I need to dig into this:
pokemon.types.first?.type.name
I would like to know if I have instead a way of getting the PokemonTypeNameContent array in the Pokemon struct, to do something like this:
struct Pokemon {
var types: [PokemonTypeNameContent]?
}
(I am not interested in getting the slot values).
Thank you for your help!
You can do custom encoding for PokemonTypeNameContent, and traverse through the levels of JSON using nestedContainer
enum PokemonTypeNameContent: String, Decodable {
case flying = "flying"
case rock = "rock"
// ...
enum OuterCodingKeys: CodingKey { case type }
enum InnerCodingKeys: CodingKey { case name }
init(from decoder: Decoder) throws {
// this is the container for each JSON object in the "types" array
let container = try decoder.container(keyedBy: OuterCodingKeys.self)
// this finds the nested container (i.e. JSON object) associated with the key "type"
let innerContainer = try container.nestedContainer(keyedBy: InnerCodingKeys.self, forKey: .type)
// now we can decode "name" as a string
let name = try innerContainer.decode(String.self, forKey: .name)
if let pokemonType = Self.init(rawValue: name) {
self = pokemonType
} else {
throw DecodingError.typeMismatch(
PokemonTypeNameContent.self,
.init(codingPath: innerContainer.codingPath + [InnerCodingKeys.name],
debugDescription: "Unknown pokemon type '\(name)'",
underlyingError: nil
)
)
}
}
}
// Pokemon can then be declared like this:
struct Pokemon: Decodable {
let id: Int
let name: String
let types: [PokemonTypeNameContent]
}
Do note that this means that you lose the option of decoding PokemonTypeNameContent as a regular enum. If you do want to do that, put the custom decoding code into a property wrapper. Note that we would be decoding the entire JSON array, instead of each JSON object.
#propertyWrapper
struct DecodePokemonTypes: Decodable {
var wrappedValue: [PokemonTypeNameContent]
init(wrappedValue: [PokemonTypeNameContent]) {
self.wrappedValue = wrappedValue
}
enum OuterCodingKeys: CodingKey { case type }
enum InnerCodingKeys: CodingKey { case name }
init(from decoder: Decoder) throws {
// container for the "types" JSON array
var unkeyedContainer = try decoder.unkeyedContainer()
wrappedValue = []
// while we are not at the end of the JSON array
while !unkeyedContainer.isAtEnd {
// similar to the first code snippet
let container = try unkeyedContainer.nestedContainer(keyedBy: OuterCodingKeys.self)
let innerContainer = try container.nestedContainer(keyedBy: InnerCodingKeys.self, forKey: .type)
let name = try innerContainer.decode(String.self, forKey: .name)
if let pokemonType = PokemonTypeNameContent(rawValue: name) {
wrappedValue.append(pokemonType)
} else {
throw DecodingError.typeMismatch(
PokemonTypeNameContent.self,
.init(codingPath: innerContainer.codingPath + [InnerCodingKeys.name],
debugDescription: "Unknown pokemon type '\(name)'",
underlyingError: nil
)
)
}
}
}
}
// You would write this in Pokemon
#DecodePokemonTypes
var types: [PokemonTypeNameContent]

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

Populate tableview with Firebase realtime database

Currently I get data from json file located on http server
if let url = NSURL(string: "https://test.com/test") {
do {
let data = try Data(contentsOf: url as URL)
let decoder = JSONDecoder()
jsonData = try decoder.decode(TestList.self, from: data)
} catch {
print("error:\(error)")
}
struct TestList: Decodable {
enum CodingKeys: String, CodingKey {
case items
}
let items: [Item]
}
struct Item: Decodable {
var item_type: String?
//...
enum CodingKeys: String, CodingKey {
case item_type
//...
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.item_type = try? container.decode(String.self, forKey: .mtype)
//...
}
}
Everything works fine but when
i switched to firebase realtime database and get the same json data
Database.database().reference().child("items").observeSingleEvent(of: .value, with: { snapshot in
guard let value = snapshot.value else { return }
do {
self.jsonData = try FirebaseDecoder().decode(TestList.self, from: value)
} catch let error {
print(error)
}
})
My JSON:
{
"items": [
{
"item_type": "Rap",
"1": "Kastro",
"2": "EDI",
"3": "Noble",
"4": ""
},
{
"item_type": "Rock",
"1": "Nickelback",
"2": "",
"3": "",
"4": ""
},
{
"item_type": "Pop",
"1": "Ringo",
"2": "",
"3": "",
"4": ""
}
]
}
got this message:
typeMismatch(Swift.Dictionary,
Swift.DecodingError.Context(codingPath: [], debugDescription: "Not a
dictionary", underlyingError: nil))
How to fix this error ?
I had this problem before, this is what I did to fix it. So let's say this is how my database looks like. Pay attention to the naming convention, in this example its snake case.
Let's say I want to create an array of users from this snapshot and populate my tableView. This is tricky cause the snapshot.value isn't really JSON and has a value of Any. This is probably why your app is crashing or in your case type is mismatch.
Lets create a model for our users. It will look something like this.
class User: Codable {
var firstName: String
var lastName: String
var ageNumber: Int
enum CodingKeys: String, CodingKey {
case firstName,lastName
case ageNumber = "age"
}
}
So let me point something our really fast. In my firebase database the user properties look like this 'first_name', 'age' 'last_name'. In my user model it look like this 'firstName', 'ageNumber' 'lastName'. So I had to add the coding keys because of the age not because of the first or last name. The first & last name changes by themselves when I set the keyCodingStrategy on my decoder.
This is how that code looks like. Left some comments to better explain what the code does.
var items = [Item]()
override func viewDidLoad() {
super.viewDidLoad()
// Use your database reference here.
let ref = Database.database().reference(withPath: "users")
ref.observe(.value) { (snapshot) in
//First create a dict out of the snapshot value
guard let dict = snapshot.value as? [String: Any] else { return }
//Create a decoder this is why I don't need to chage the first_name to firstName
//inside my coding keys
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
//The key is the UID and the value is what we need to create a new users
dict.forEach { (key, value) in
do {
//Create new user and add it to our users array
//We convert the value to data for the decoder with this line of code
let testListData = try JSONSerialization.data(withJSONObject: value, options: .prettyPrinted)
let testList = try decoder.decode(TestList.self, from: testListData)
testList.items.forEach({ (item) in
self.items.append(item)
})
}
catch let err {
print(err.localizedDescription)
}
}
//Reload Table View
}
}
Hope this helps.
Edited
So based on the JSON you provided. You will have to create your structs something like this.
struct TestList: Codable {
var items: [Item]
}
struct Item: Codable {
var itemType: String
var one: String
var two: String
var three: String
var four: String
enum CodingKeys: String, CodingKey {
case itemType
case one = "1"
case two = "2"
case three = "3"
case four = "4"
}
}
Hopefully this will solve your problem.

Extracting data from JSON array with swift Codable

I have a JSON response like this:
I have currently designed my decodable struct to be as follows:
struct PortfolioResponseModel: Decodable {
var dataset: Dataset
struct Dataset: Decodable {
var data: Array<PortfolioData> //I cannot use [Any] here...
struct PortfolioData: Decodable {
//how to extract this data ?
}
}
}
The question is, how do I extract the data inside the array, which can have a value Double or String.
Here is the sample string to make this work on playground:
let myJSONArray =
"""
{
"dataset": {
"data": [
[
"2018-01-19",
181.29
],
[
"2018-01-18",
179.8
],
[
"2018-01-17",
177.6
],
[
"2018-01-16",
178.39
]
]
}
}
"""
Extracting the data:
do {
let details2: PortfolioResponseModel = try JSONDecoder().decode(PortfolioResponseModel.self, from: myJSONArray.data(using: .utf8)!)
//print(details2)
//print(details2.dataset.data[0]) //somehow get "2018-01-19"
} catch {
print(error)
}
I cannot use [Any] here.
Never use Any when decoding JSON because usually you do know the type of the contents.
To decode an array you have to use an unkeyedContainer and decode the values in series
struct PortfolioResponseModel: Decodable {
var dataset: Dataset
struct Dataset: Decodable {
var data: [PortfolioData]
struct PortfolioData: Decodable {
let date : String
let value : Double
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
date = try container.decode(String.self)
value = try container.decode(Double.self)
}
}
}
}
You can even decode the date strings as Date
struct PortfolioData: Decodable {
let date : Date
let value : Double
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
date = try container.decode(Date.self)
value = try container.decode(Double.self)
}
}
if you add a date formatter to the decoder
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(formatter)
let details2 = try decoder.decode(PortfolioResponseModel.self, from: Data(myJSONArray.utf8))
To add to this, there is a very good example of complex JSON parsing with arrays in particular here. I hope this helps those who are trying to use Codeable with larger, more realistic JSON data.
The overview is this: Imagine you had the following JSON format:
{
"meta": {
"page": 1,
"total_pages": 4,
"per_page": 10,
"total_records": 38
},
"breweries": [
{
"id": 1234,
"name": "Saint Arnold"
},
{
"id": 52892,
"name": "Buffalo Bayou"
}
]
}
This is a common format with the array nested inside. You could create a struct that encapsulates the entire response, accommodating arrays for the "breweries" key, similar to what you were asking above:
struct PagedBreweries : Codable {
struct Meta : Codable {
let page: Int
let totalPages: Int
let perPage: Int
let totalRecords: Int
enum CodingKeys : String, CodingKey {
case page
case totalPages = "total_pages"
case perPage = "per_page"
case totalRecords = "total_records"
}
}
struct Brewery : Codable {
let id: Int
let name: String
}
let meta: Meta
let breweries: [Brewery]
}

Swift // Convert a String Json file to an enum case

I have the object below:
class Food {
var cal: Int
var displayName: String
var imgUrl: String
var dishType: DishType
init(cal: Int, displayName: String, imgUrl: String, dishType: DishType) {
self.cal = cal
self.displayName = displayName
self.imgUrl = imgUrl
self.dishType = dishtype
}
}
enum DishType {
case starter
case main
case desert
}
And this is a part of my Alamofire request:
if let cal = foodJson["cal"].int,
let displayName = foodJson["display_name"].string,
let dishType = foodJson["type"].string,
let imgUrl = foodJson["imgUrl"].string {
let food = Food(cal: cal, displayName: displayName, imgUrl: imgUrl, dishType: ??)
foods.append(food)
How can I convert the Json String "dishType" into a "DishType" type I created with the enum in order to correctly fill my instance of Food?
You might want to specify an associated value for your enum:
enum DishType: String {
case starter = "starter"
case main = "main"
case desert = "desert"
}
Or, more simply:
enum DishType: String {
case starter
case main
case desert
}
Then you can do:
dishType = DishType(rawValue: string)
e.g.
if let dishTypeString = foodJson["type"].string,
let dishType = DishType(rawValue: dishTypeString) {
...
}
Personally, if doing Swift 4, I'd retire SwiftyJSON and use the native JSONDecoder and declare your types to be Codable. (Note, we still need to define the DishType to have associated values, like above.)
For example, let's imagine your response was something like:
{
"foods": [{
"cal": 800,
"display_name": "Beef",
"imgUrl": "http://example.com/wheres_the_beef.jpg",
"dishType": "main"
},
{
"cal": 2000,
"display_name": "Chocolate cake",
"imgUrl": "http://example.com/yummy.jpg",
"dishType": "desert"
}
]
}
You could then define your types like so:
struct Food: Codable {
let cal: Int
let displayName: String
let imgUrl: String
let dishType: DishType
}
enum DishType: String, Codable {
case starter
case main
case desert
}
And then you can parse the response like so:
struct FoodsResponse: Codable {
let foods: [Food]
}
Alamofire.request(url)
.responseData { response in
switch response.result {
case .success(let data):
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let responseObject = try decoder.decode(FoodsResponse.self, from: data)
print(responseObject.foods)
} catch {
print(error)
}
case .failure(let error):
print(error)
}
}
This gets you completely out of the business of manually iterating through the results to map it to your objects.
Clearly, I assume your real response has more keys than just foods, so you'd add whatever fields you needed to FoodsResponse, but hopefully this illustrates the idea of letting JSONDecoder parse the JSON into your model structures automatically.
For more information about JSONDecoder and Codable types, see Encoding and Decoding Custom Types.
By the way, my example FoodResponse structure prompted some question why I didn't just assume the web service would return an array of Food objects. Let me explain my rationale.
A more typical structure for FoodsResponse in a web service response would be something like:
struct FoodsResponse: Codable {
let success: Bool
let error: String? // only supplied if `success` was `false`
let foods: [Food]? // only supplied if `success` was `true`
}
In this structure, this response object can handle success scenarios, like:
{
"success": true,
"foods": [...]
}
Or failures:
{
"success": false,
"error": "No data found"
}
I think it best to have a structure that includes some common success Boolean, e.g. success, that all well-formed responses include, and then have various properties that are filled in for successes or failures, respectively.