I am stuck on parsing JSON. The structure is really hard. I was trying this with a decodable approach.
import UIKit
struct WeatherItem: Decodable {
let title: String?
let value: String?
let condition: String?
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
print("hello")
let jsonUrlString = "http://virtualflight.ddns.net/api/weather.php?icao=ehrd"
guard let url = URL(string: jsonUrlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
var arr = [WeatherItem]()
do {
let res = try JSONDecoder().decode([String:[[String]]].self, from: data)
let content = res["title"]!
content.forEach {
if $0.count >= 3 {
arr.append(WeatherItem(title:$0[0],value:$0[1],condition:$0[2]))
}
}
print(arr)
} catch {
print(error)
}
}
}
}
The json is the following:
{
"temperature": {
"value_c": 11,
"value_f": 285,
"condition": "Good",
"value_app": "11 \u00b0C (285 \u00b0F)"
},
"visibility": {
"value_km": 10,
"value_m": 6.2,
"condition": "Good",
"value_app": "10 KM (6.2 Mi)"
},
"pressure": {
"value_hg": 29.4,
"value_hpa": 996,
"condition": "Good",
"value_app": "29.4 inHg (996 hPa)"
},
"wind": {
"value_kts": 20,
"value_kmh": 37,
"value_heading": 280,
"condition": "Bad",
"value_app": "280\u00b0 at 20 KTS (37 Km\/H)"
},
"station": "EHRD",
"metar": "EHRD 141355Z AUTO 28020KT 250V320 9999 SCT038 BKN043 BKN048 11\/07 Q0996 NOSIG",
"remarks": "NOSIG",
"weather_page_ios_simple": [
[
"Temperature",
"11 \u00b0C (285 \u00b0F)",
"Good"
],
[
"Visibility",
"10 KM (6.2 Mi)",
"Good"
],
[
"Pressure",
"29.4 inHg (996 hPa)",
"Good"
],
[
"Wind",
"280\u00b0 at 20 KTS (37 Km\/H)",
"Bad"
],
[
"Metar",
"EHRD 141355Z AUTO 28020KT 250V320 9999 SCT038 BKN043 BKN048 11\/07 Q0996 NOSIG",
"Unknown"
],
[
"Remarks",
"NOSIG",
"Unknown"
],
[
"Station",
"EHRD",
"Unknown"
],
[
"UICell",
"iOS 12",
"siri_weather_cell"
]
]
}
any ideas how to do this?? I only need the last array, weather_page_ios_simple.
Have a look at https://app.quicktype.io it will give you the data structure for your JSON.
import Foundation
struct Welcome: Codable {
let temperature: Temperature
let visibility: Visibility
let pressure: Pressure
let wind: Wind
let station, metar, remarks: String
let weatherPageIosSimple: [[String]]
enum CodingKeys: String, CodingKey {
case temperature, visibility, pressure, wind, station, metar, remarks
case weatherPageIosSimple = "weather_page_ios_simple"
}
}
struct Pressure: Codable {
let valueHg: Double
let valueHpa: Int
let condition, valueApp: String
enum CodingKeys: String, CodingKey {
case valueHg = "value_hg"
case valueHpa = "value_hpa"
case condition
case valueApp = "value_app"
}
}
struct Temperature: Codable {
let valueC, valueF: Int
let condition, valueApp: String
enum CodingKeys: String, CodingKey {
case valueC = "value_c"
case valueF = "value_f"
case condition
case valueApp = "value_app"
}
}
struct Visibility: Codable {
let valueKM: Int
let valueM: Double
let condition, valueApp: String
enum CodingKeys: String, CodingKey {
case valueKM = "value_km"
case valueM = "value_m"
case condition
case valueApp = "value_app"
}
}
struct Wind: Codable {
let valueKts, valueKmh, valueHeading: Int
let condition, valueApp: String
enum CodingKeys: String, CodingKey {
case valueKts = "value_kts"
case valueKmh = "value_kmh"
case valueHeading = "value_heading"
case condition
case valueApp = "value_app"
}
}
If you only need the bottom array of data, you shouldn't need to put everything into the decoded struct. Just decode the part of the response you want and pull the data from there. Also, that array of data isn't really parsing JSON without keys. Its just an array of strings, you'll have to rely on the fact that index 0 is always title, 1 is always value, and 2 is always condition. Just do some validation to make sure it fits your needs. Something like this (UNTESTED)
struct WeatherItem {
let title: String?
let value: String?
let condition: String?
init(title: String?, value: String?, condition: String?) {
self.title = title
self.value = value
self.condition = condition
}
}
struct WeatherResponse: Decodable {
var weatherItems: [WeatherItem]
private enum CodingKeys: String, CodingKey {
case weatherItems = "weather_page_ios_simple"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let weatherItemArrays = try container.decode([[String]].self, forKey: .weatherItems)
weatherItems = []
for weatherItemArray in weatherItemArrays {
var title: String?
if weatherItemArray.count > 0 {
title = weatherItemArray[0]
}
var value: String?
if weatherItemArray.count > 1 {
value = weatherItemArray[1]
}
var condition: String?
if weatherItemArray.count > 2 {
condition = weatherItemArray[2]
}
weatherItems.append(WeatherItem(title: title, value: value, condition: condition))
}
}
}
And then when you get your api response get the weather items out with something like
do {
let weatherResponse = try JSONDecoder().decode(WeatherResponse.self, from: <YOUR API RESPONSE DATA>)
let weatherItems = weatherResponse.weatherItems
<DO WHATEVER YOU WANT WITH THE WEATHER ITEMS>
} catch let error {
print(error)
}
Related
I am trying to handle a JSON with Codable but there's a parsing error when I decode it with JSONDecoder().decode.
{
"data": [
{
"id": "90",
"symbol": "BTC",
"name": "Bitcoin",
"nameid": "bitcoin",
"rank": 1,
"price_usd": "50513.75",
"percent_change_24h": "3.03",
"percent_change_1h": "-0.50",
"percent_change_7d": "-9.91",
"price_btc": "1.00",
"market_cap_usd": "942710364520.73",
"volume24": 70745042591.75044,
"volume24a": 107034995571.4168,
"csupply": "18662452.00",
"tsupply": "18662452",
"msupply": "21000000"
},
{
"id": "80",
"symbol": "ETH",
"name": "Ethereum",
"nameid": "ethereum",
"rank": 2,
"price_usd": "4052.44",
"percent_change_24h": "10.17",
"percent_change_1h": "-0.78",
"percent_change_7d": "17.75",
"price_btc": "0.084812",
"market_cap_usd": "466734637594.73",
"volume24": 53134000887.50444,
"volume24a": 87082811090.79503,
"csupply": "115173595.00",
"tsupply": "115173595",
"msupply": ""
}
],
"info": {
"coins_num": 5949,
"time": 1621022046
}
}
The json is like above and I coded all my models like below.
class CoinModel: Codable {
var data: [Coin]?
var info: CoinInfo?
enum CodingKeys: String, CodingKey {
case data
case info
}
}
class CoinInfo: Codable {
var coinsNum: Int?
var time: TimeInterval?
enum CodingKeys: String, CodingKey {
case coinsNum = "coins_num"
case time = "time"
}
}
class Coin: Codable{
var csupply: String?
var id: String?
var marketCapUsd: String?
var msupply: Int?
var name: String?
var nameid: String?
var percentChange1h: String?
var percentChange24h: String?
var percentChange7d: String?
var priceBtc: String?
var priceUsd: String?
var rank: Int?
var symbol: String?
var tsupply: Int?
var volume24: Double?
var volume24a: Double?
enum CodingKeys: String, CodingKey {
case csupply = "csupply"
case id = "id"
case marketCapUsd = "market_cap_usd"
case msupply = "msupply"
case name = "name"
case nameid = "nameid"
case percentChange1h = "percent_change_1h"
case percentChange24h = "percent_change_24h"
case percentChange7d = "percent_change_7d"
case priceBtc = "price_btc"
case priceUsd = "price_usd"
case rank = "rank"
case symbol = "symbol"
case tsupply = "tsupply"
case volume24 = "volume24"
case volume24a = "volume24a"
}
}
And my network function work like below.
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard error == nil else {
completion(.failure(error!))
return
}
guard let data = data else {
completion(.failure(error ?? NSError()))
return
}
let json = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as! [String: Any]
guard let model = try? JSONDecoder().decode(CoinModel.self, from: data) else {
completion(.success([]))
return
}
completion(.success(model.data ?? []))
}.resume()
As I said before, json object is not nil and I got like below.
[({
csupply = "435032301.00";
id = 33285;
"market_cap_usd" = "435337535.24";
msupply = "";
name = "USD Coin";
nameid = "usd-coin";
"percent_change_1h" = "0.01";
"percent_change_24h" = "0.19";
"percent_change_7d" = "0.16";
"price_btc" = "0.000023";
"price_usd" = "1.00";
rank = 100;
symbol = USDC;
tsupply = 435032301;
volume24 = "11520659193.03477";
volume24a = "13684311331.83874";
}
)
, "info": {
"coins_num" = 5952;
time = 1621160044;
}]
I can handle the JSON with JSONSerialization but I could not find any reason for parse error in the JSONDecoder way. I think I did not see the problem. Thanks for any advice and helps.
Edit:
I solved it with several changes in my Codable class, I made a mistake when I describing model for id, csupply and etc.
var csupply: String?
var msupply: String?
var tsupply: String?
You have several mistakes on defining object. For example you have defined id, msupply is a integer but they are string. You have defined the volume24 and volume24a is string but they are not. You need to fix all of these probably thats why you can't parse it.
All wrong defined parameters are id, mSupply, volume24, volume24a, tSupply for coin object and for the info object you need to convert time to Int aswell.
can someone explain to me what am I doing wrong in this case, it goes into case success but it throws a thread The «Unexpectedly found nil while unwrapping an Optional value» Error at the line self.actions = dataRoles.data like highlighting .data
The JSON looks like
{
"company_id": 1,
"data": [
{
"code": "master",
"id": 1,
"inserted_at": "01/02/2021 09:38",
"name": "Master Admin"
},
{
"code": "user",
"id": 2,
"inserted_at": "01/02/2021 09:38",
"name": "User"
}
],
"draw": 1,
"page_number": 1,
"page_size": 100,
"recordsFiltered": 2,
"recordsTotal": 2,
"total_pages": 1
}
The Struct is
import Foundation
struct ListRoles: Codable {
let companyID: Int!
let data: [DataRoles]!
let draw, pageNumber, pageSize, recordsFiltered: Int!
let recordsTotal, totalPages: Int!
private enum CodingKeys: String, CodingKey {
case companyID = "company_id"
case data, draw
case pageNumber = "page_number"
case pageSize = "page_size"
case recordsFiltered, recordsTotal
case totalPages = "total_pages"
}
}
struct DataRoles: Codable {
let code: String!
let id: Int!
let insertedAt, name: String!
private enum CodingKeys: String, CodingKey {
case code, id
case insertedAt = "inserted_at"
case name
}
}
And the ViewModel that I am working on looks like this
class ListRolesViewModel {
weak var delegate: ListRolesViewModelDelegate?
private var action: ListRoles!
private var actions: [DataRoles] = []
func getRoles() {
guard let accessToken = KeychainWrapper.standard.string(forKey: "codaxim_access_token") else { return }
self.delegate?.willGetRoles(self)
Alamofire.request(Url.shared.listAllRoles, method: .get, headers: ["Authorization" : "Bearer " + accessToken])
.responseJSON { response in
switch response.result {
case .success:
guard let dataRoles = response.data?.decode(for: ListRoles.self) else { return }
self.action = dataRoles
self.actions = dataRoles.data
self.delegate?.didGetRoles(self)
print("Success")
break
case .failure:
self.delegate?.didFailGetRoles(self)
print("Alamofire error: \(String(describing: response.result.error))")
break
}
}
}
}
I have tried a couple of ways and none it seems to work, from changing the guard let to if let, adding ! in my struct... If you have any idea what it can be it would help me a lot! Thanks in advance.
I want to show car data in tableview , and there is 2 type of history model. As you can see in my response 1st object of car have history and other object have None.
How to make Structure of History json model in structure of VehicalModel , how to access that model and map with the help of alamofire. And How to check the history is available or not if available then store in model and show in tableview.
This is my Response
{
"response": "success",
"account_type": "2",
"car_data": [
{
"registration_no": "Lzq 2233",
"engincc": "600 - 999",
"enginccID": "1",
"vehicleID": "32",
"history": [
{
"packages": "",
"date_time": "2018-12-22 00:40:55",
"bill_amount": "7098",
"bill_discount": "133.0571251",
"bill_paid": "36070"
}
]
},
{
"registration_no": "ghfdhhh",
"engincc": "1500 - 1799",
"enginccID": "3",
"vehicleID": "33",
"history": "None"
}
]
}
This is My Model
struct VehicleDataModel {
var registrationNo : String?
var engineCC: String?
var engineCCID: String?
var vehicleID: String?
var history: [HistoryModel]
struct HistoryModel {
var packages: String
var billDiscount : String
var dateTime: String
var billPaid: String
var billAmount: String
}
}
This is my Call API Function:
func callApi() {
let url = "http://esspk.net/production/20m/Api/getVehicleApi"
let userID = UserDefaults.standard.integer(forKey: "user_id")
let param = ["user_id" : userID]
print(param)
ServerCall.makeCallWitoutFile(url, params: param, type: Method.POST, currentView: nil) { (response) in
if let json = response {
print(json)
if let carData = json["car_data"].array
{
//let vehicalObj = VehicleDataModel()
for cData in carData {
let regNo = cData["registration_no"].string
let enginCC = cData["engincc"].string
let enginID = cData["enginccID"].string
let vehicleID = cData["vehicleID"].string
let history = cData["history"].arrayObject
// let vech = VehicleDataModel(registrationNo: regNo, engineCC: enginCC, engineCCID: enginID, vehicleID: vehicle, history: history)
// self.vehicalModel.append(vech)
// let vech = VehicleDataModel.init(registrationNo: regNo, engineCC: enginCC, engineCCID: enginID, vehicleID: vehicleID, history: VehicleDataModel.HistoryModel( )
}
self.myVehicleTblView.reloadData()
}
}
}
}
First Make Model Class.
class Welcome: Codable {
let response, accountType: String
let carData: [CarDatum]
enum CodingKeys: String, CodingKey {
case response
case accountType = "account_type"
case carData = "car_data"
}
init(response: String, accountType: String, carData: [CarDatum]) {
self.response = response
self.accountType = accountType
self.carData = carData
}
}
class CarDatum: Codable {
let registrationNo, engincc, enginccID, vehicleID: String
let history: HistoryUnion
enum CodingKeys: String, CodingKey {
case registrationNo = "registration_no"
case engincc, enginccID, vehicleID, history
}
init(registrationNo: String, engincc: String, enginccID: String, vehicleID: String, history: HistoryUnion) {
self.registrationNo = registrationNo
self.engincc = engincc
self.enginccID = enginccID
self.vehicleID = vehicleID
self.history = history
}
}
enum HistoryUnion: Codable {
case historyElementArray([HistoryElement])
case string(String)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let x = try? container.decode([HistoryElement].self) {
self = .historyElementArray(x)
return
}
if let x = try? container.decode(String.self) {
self = .string(x)
return
}
throw DecodingError.typeMismatch(HistoryUnion.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for HistoryUnion"))
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .historyElementArray(let x):
try container.encode(x)
case .string(let x):
try container.encode(x)
}
}
}
class HistoryElement: Codable {
let packages, dateTime, billAmount, billDiscount: String
let billPaid: String
enum CodingKeys: String, CodingKey {
case packages
case dateTime = "date_time"
case billAmount = "bill_amount"
case billDiscount = "bill_discount"
case billPaid = "bill_paid"
}
init(packages: String, dateTime: String, billAmount: String, billDiscount: String, billPaid: String) {
self.packages = packages
self.dateTime = dateTime
self.billAmount = billAmount
self.billDiscount = billDiscount
self.billPaid = billPaid
}
}
Or you can also Use below Class
struct Welcome {
let response, accountType: String
let carData: [CarDatum]
}
struct CarDatum {
let registrationNo, engincc, enginccID, vehicleID: String
let history: HistoryUnion
}
enum HistoryUnion {
case historyElementArray([HistoryElement])
case string(String)
}
struct HistoryElement {
let packages, dateTime, billAmount, billDiscount: String
let billPaid: String
}
Then I recommended to use the Alamofire for Call the API.
func request() {
let url = URL(string: "my url")
Alamofire.request(url!).responseJSON {(response) in
switch (response.result) {
case .success:
if let data = response.data {
do {
let response = try JSONDecoder().decode([Welcome].self, from: data)
self.arrList = response
DispatchQueue.main.async {
//Print Responce
}
} catch {
print(error.localizedDescription)
}
}
case .failure( let error):
print(error)
}
}
}
}
Hope This Works.
I've got the following structures to aid in returning data from a JSON web api:
// To parse the JSON, add this file to your project and do:
//
// let story = try? JSONDecoder().decode(Story.self, from: jsonData)
import Foundation
typealias Story = [StoryElement]
struct StoryElement: Codable {
let id: Int
let url: String
let storyPublic, featured: Bool
let added, modified: String
let itemType: ItemType
let collection: JSONNull?
let owner: Owner
let files: Files
let tags: [ItemType]
let elementTexts: [ElementText]
let extendedResources: ExtendedResources
enum CodingKeys: String, CodingKey {
case id, url
case storyPublic = "public"
case featured, added, modified
case itemType = "item_type"
case collection, owner, files, tags
case elementTexts = "element_texts"
case extendedResources = "extended_resources"
}
}
struct ElementText: Codable {
let html: Bool
let text: String
let elementSet: ElementSet
let element: Element
enum CodingKeys: String, CodingKey {
case html, text
case elementSet = "element_set"
case element
}
}
struct Element: Codable {
let id: Int
let url, name: String
let resource: ElementResource
}
enum ElementResource: String, Codable {
case elements = "elements"
}
struct ElementSet: Codable {
let id: Int
let url: URL
let name: Name
let resource: ElementSetResource
}
enum Name: String, Codable {
case dublinCore = "Dublin Core"
case itemTypeMetadata = "Item Type Metadata"
}
enum ElementSetResource: String, Codable {
case elementSets = "element_sets"
}
enum URL: String, Codable {
case httpWWWRalstoncemeteryCOMGreeleyAPIElementSets1 = "http://www.ralstoncemetery.com/greeley/api/element_sets/1"
case httpWWWRalstoncemeteryCOMGreeleyAPIElementSets3 = "http://www.ralstoncemetery.com/greeley/api/element_sets/3"
}
struct ExtendedResources: Codable {
let exhibitPages: Files
let geolocations: Owner
enum CodingKeys: String, CodingKey {
case exhibitPages = "exhibit_pages"
case geolocations
}
}
struct Files: Codable {
let count: Int
let url, resource: String
}
struct Owner: Codable {
let id: Int
let url, resource: String
}
struct ItemType: Codable {
let id: Int
let url, name, resource: String
}
// MARK: Encode/decode helpers
class JSONNull: Codable {
public init() {}
public required init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if !container.decodeNil() {
throw DecodingError.typeMismatch(JSONNull.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for JSONNull"))
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encodeNil()
}
}
You can see the JSON output I am working with, as well as the original structures here: https://app.quicktype.io?share=oAMNjooSzgpraWpf1KIj (just for reference).
The data (I believe) is returned successfully and parsed correctly, the only issue comes down to myself not being as familiar (I'm in the process of trying to learn a bit more).
So I have a TableViewController and in that I have a cell with the following coded into it:
struct StoryCellViewModel {
let id: Int
let url: String
let storyPublic, featured: Bool
let added, modified: String
}
And in the actual TableViewController under the viewDidLoad() I have this portion of script:
print(story)
self.cellViewModels = story.map{
StoryCellViewModel(id: $0.id, url: $0.url, storyPublic: $0.storyPublic, featured: $0.featured, added: $0.added, modified: $0.modified)
}
and a little bit lower than that, I have:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "StoryCell", for: indexPath)
let cellViewModel = cellViewModels[indexPath.row]
cell.textLabel?.text = String(cellViewModel.id)
cell.detailTextLabel?.text = cellViewModel.modified
return cell
}
I will make it known that all of this works, the id and modified (both just being used as tests to ensure the connection and printing works properly) are fine, but my question comes up here:
If we go back to the structures there is this portion:
struct ElementText: Codable {
let html: Bool
let text: String
let elementSet: ElementSet
let element: Element
enum CodingKeys: String, CodingKey {
case html, text
case elementSet = "element_set"
case element
}
}
Which refers to the following portion of example JSON:
"element_texts": [
{
"html": false,
"text": "Woehler and Force Farm Equipment Building",
"element_set": {
"id": 1,
"url": "http://www.ralstoncemetery.com/greeley/api/element_sets/1",
"name": "Dublin Core",
"resource": "element_sets"
},
"element": {
"id": 50,
"url": "http://www.ralstoncemetery.com/greeley/api/elements/50",
"name": "Title",
"resource": "elements"
}
},
{
"html": false,
"text": "Woehler and Force Farm Equipment",
"element_set": {
"id": 1,
"url": "http://www.ralstoncemetery.com/greeley/api/element_sets/1",
"name": "Dublin Core",
"resource": "element_sets"
},
"element": {
"id": 39,
"url": "http://www.ralstoncemetery.com/greeley/api/elements/39",
"name": "Creator",
"resource": "elements"
}
},
{
"html": false,
"text": "Street view of the front exterior of Woehler and Force Farm Equipment. Several automobiles are visible through the windows of the store. The alley along the side of the building is also visible. There are several signs along the front of the building reading, 'Farm Equipment,' 'Kaiser Frazer W&F,' and 'Woehler & Force.'; Verso There is a sticker with typed black ink reading, 'Woehler & Force 1316-22 8th Ave. - Greeley, CO 1947. Orig. env. says Liberty Trucker Parts Co./ 690 Lincoln St./F.V. Altwater/POB 1889/Denver, Colo.'",
"element_set": {
"id": 3,
"url": "http://www.ralstoncemetery.com/greeley/api/element_sets/3",
"name": "Item Type Metadata",
"resource": "element_sets"
},
"element": {
"id": 54,
"url": "http://www.ralstoncemetery.com/greeley/api/elements/54",
"name": "Story",
"resource": "elements"
}
}
],
**So my question is: ** How would I go about printing out say the first of the element_texts (The one that has "Woehler and Force Farm Equipment Building" set for the text field)?
If there is any further explanation required, I'll be happy to type it up. Or if anyone has any resources for this level of nesting, I'd be very grateful. Thank you -
story is an array of StoryElement (why not stories?)
The type of property elementTexts in StoryElement is an array of ElementText
So basically you need two loops to iterate over story and elementTexts
for aStory in story {
for elementText in aStory.elementTexts {
print(elementText.text)
}
}
First of all in "CellForRowat" call let cell = dequeueReusableCellWithIdentifier:#"cell" forIndexPath:indexPath etc.
then place text in cell.text
Create a array like this . var AllTexts = [String]()
you can fetch json data so many ways like JsonDecode/JSONSerialization, i am using JSONSerialization. After fetching data i am appending text values in to array.
do {
let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe)
let jsonResult = try JSONSerialization.jsonObject(with: data, options: .mutableLeaves) as! [String: Any]
for (key,value) in jsonResult {
print(key)
if let result2:[[String:Any]] = value as? [[String:Any]]{
for dict in result2 {
for (key,value) in dict {
if key == "text" {
self.AllTexts.append(value as! String)
}
}
}
}
}
print(self.AllTexts)
} catch {
// handle error
print(error)
}
My dilemma is I'm receiving two different object types from one table in a JSON response. Here is an example of the response of both types in a return.
"supplementaryItems": [
{
"header": "Doodle",
"subHeader": "It's a drawing.",
"slideID": 4,
"imageName": null,
"textItems": null,
"sortOrder": 0
},
{
"header": "Cell Phones",
"subHeader": "No phones please",
"slideID": 8,
"imageName": "welcome_icon_cellphones",
"textItems": ["first","second","third"],
"sortOrder": 1
}
]
What we're hoping to do is create two different types of objects here. A textOnlyItem, and a imageWithTextItem.
Is there a way to create one as a subclass or extension that can be keyed off of a Bool defined by whether imageName is null or not?
Thanks for any help all.
You don't need two different objects. Just declare imageName and textItems as optional, this handles the null case.
You can simply check whether imageName is nil
let jsonString = """
{"supplementaryItems": [
{
"header": "Doodle",
"subHeader": "It's a drawing.",
"slideID": 4,
"imageName": null,
"textItems": null,
"sortOrder": 0
},
{
"header": "Cell Phones",
"subHeader": "No phones please",
"slideID": 8,
"imageName": "welcome_icon_cellphones",
"textItems": ["first","second","third"],
"sortOrder": 1
}
]
}
"""
struct Root : Decodable {
let supplementaryItems : [SupplementaryItem]
}
struct SupplementaryItem : Decodable {
let header : String
let subHeader : String
let slideID : Int
let imageName : String?
let textItems : [String]?
let sortOrder : Int
}
do {
let data = Data(jsonString.utf8)
let result = try JSONDecoder().decode(Root.self, from: data)
for item in result.supplementaryItems {
if let imageName = item.imageName {
print(imageName + " has text items")
} else {
print(item.header + " has no text items")
}
}
} catch { print(error) }
I actually like vadian's approach, of one type. But I gather that would require significant refactoring in your situation.
The other approach is to just use JSONSerialization and build your heterogeneous array manually. JSONSerialization isn't deprecated, it just doesn't do it automatically like JSONDecoder.
Another approach is to use JSONDecoder, writing custom initializer that tries to decode it as ImageItem, and if that fails, try decoding it as TextItem:
protocol SupplementaryItem {
var header: String { get }
var subHeader: String { get }
var slideID: Int { get }
var sortOrder: Int { get }
var textItems: [String]? { get }
}
struct TextItem: SupplementaryItem, Codable {
let header: String
let subHeader: String
let slideID: Int
let sortOrder: Int
let textItems: [String]?
}
struct ImageItem: SupplementaryItem, Codable {
let header: String
let subHeader: String
let slideID: Int
let sortOrder: Int
let textItems: [String]?
let imageName: String
}
struct ResponseObject: Decodable {
let supplementaryItems: [SupplementaryItem]
enum CodingKeys: String, CodingKey {
case supplementaryItems
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
var container = try values.nestedUnkeyedContainer(forKey: .supplementaryItems)
if container.count == nil {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Expected array for supplementaryItems")
}
var items = [SupplementaryItem]()
while !container.isAtEnd {
if let item = try? container.decodeIfPresent(ImageItem.self), let imageItem = item {
items.append(imageItem)
} else {
let textItem = try container.decode(TextItem.self)
items.append(textItem)
}
}
supplementaryItems = items
}
}
Then:
let string = """
{
"supplementaryItems": [
{
"header": "Doodle",
"subHeader": "It's a drawing.",
"slideID": 4,
"imageName": "foo",
"textItems": null,
"sortOrder": 0
},
{
"header": "Cell Phones",
"subHeader": "No phones please",
"slideID": 8,
"imageName": "welcome_icon_cellphones",
"textItems": ["first","second","third"],
"sortOrder": 1
}
]
}
"""
let data = string.data(using: .utf8)!
let json = try! JSONDecoder().decode(ResponseObject.self, from: data)
print(json)
I'm not convinced this is any better or worse than just using JSONSerialization, but it's another approach.