I have a json response like so…
{
"message_code": 1,
"orderCount": 52,
"productCount": 5,
"outstandingPayment": [],
"pendingOrder": [
{
"order_id": 1,
"grand_total": 67.85
"customer_name": “xcvv”
"mobile_number": 2147483647
},
],
"bestWishes": [
{
"customer_id": 1,
"birth_date": "2018-02-02",
"type": "birth_date",
"customer_name": “xcvv”,
"mobile_number": 2147483647
},
{
"customer_id": 1,
"anniversary_date": "2018-02-02",
"type": "anniversary_date",
"customer_name": “sdfs”,
"mobile_number": 2147483647
}
]
}
To parse pendingOrder I have made a struct like so:
struct PendingOrder: Codable {
let order_id: Int
let grand_total: Double
let customer_name: String
let mobileNo: Int
init(order_id : Int, grand_total: Double, customer_name: String, mobileNo: Int) {
self.order_id = order_id
self.grand_total = grand_total
self.customer_name = customer_name
self.mobileNo = mobileNo
}
}
But how can I make a struct for bestWishes since each dictionary has dissimilar data i.e.the 1st dictionary has a field birth_date & the 2nd dictionary has a field anniversary_date...?
EDIT: While making Alamofire request, this is how I'm parsing each data and assigning them to struct..
if let bestWishes = result["bestWishes"] as? [[String:Any]] {
for anItem in bestWishes {
guard let customerId = anItem["customer_id"] as? Int,
let birthdate = anItem["birth_date"] as? String,
let customerName = anItem["customer_name"] as? String,
let mobNo = anItem["mobile_number"] as? Int,
let anniversaryDate = anItem["anniversary_date"] as? String,
let type = anItem["type"] as? String
else {continue}
let bestWishes = BestWishes(customer_id: customerId, birthDate: birthdate, type: type, customer_name: customerName, mobileNo: mobNo, anniversaryDate: anniversaryDate)
self.bestWishesArr.append(bestWishes)
First of all when using Codable basically you don't need an initializer.
Without a custom initializer (related to Codable) the most suitable solution is to declare both dates as optional
struct BestWish: Codable {
private enum CodingKeys : String, CodingKey {
case customerID = "customer_id"
case birthDate = "birth_date"
case anniversaryDate = "anniversary_date"
case type
case customerName = "customer_name"
case mobileNumber = "mobile_number"
}
let customerID: Int
let birthDate: String?
let anniversaryDate: String?
let type: String
let customerName : String
let mobileNumber: Int
}
With a custom initializer declare one property date and decode anniversary or birth depending on type
struct BestWish: Decodable {
private enum CodingKeys : String, CodingKey {
case customerID = "customer_id"
case birthDate = "birth_date"
case anniversaryDate = "anniversary_date"
case type
case customerName = "customer_name"
case mobileNumber = "mobile_number"
}
let customerID: Int
let date: Date
let type: String
let customerName : String
let mobileNumber: Int
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
customerID = try container.decode(Int.self, forKey: .customerID)
type = try container.decode(String.self, forKey: .type)
if type == "anniversary_date" {
date = try container.decode(Date.self, forKey: .anniversaryDate)
} else {
date = try container.decode(Date.self, forKey: .birthDate)
}
customerName = try container.decode(String.self, forKey: .customerName)
mobileNo = try container.decode(Int.self, forKey: .mobileNumber)
}
}
You can distinguish the dates by the given type property.
To decode also the root object you need an umbrella struct
struct Root : Decodable {
// let pendingOrder : [PendingOrder]
let bestWishes : [BestWish]
}
Rather than the deserialized dictionary you have to get the JSON string in Data format from the Alamofire request to pass it to JSONDecoder
do {
let decoder = JSONDecoder()
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
decoder.dateDecodingStrategy = .formatted(formatter)
let result = try decoder.decode(Root.self, from: data)
print(result)
} catch { print(error) }
Edit: I added code to decode both dates as Date
Use optional type
struct BestWishes: Codable {
let birth_date: Date?
let anniversary_date: Date?
init(birth_date : Date?, anniversary_date: Date?) {
self.birth_date = birth_date
self.anniversary_date = anniversary_date
}
}
Related
Decoding fails if keys not present. How to safely decode if missing keys also.
I have gone through that use Optional or Nil values but still not able to decoding the objects.
Below my Json
{
"mandatory":true,
"dynamic_obj":[
{
"dt":"2021-09-22 01:29:52",
"url":"https://res._22_01_29.pdf",
"desc":"PAN CARD",
"flag":1,
"count":"2",
"field":"pan_card",
"address":"300-435, Nattu Muthu St, Sheethammal Colony, Venus Colony, Chennai, Tamil Nadu 600018, India",
"visible":true,
"latitude":13.0389309,
"longitude":80.2473746
},
{
"url":"https://res.cloudin/no-image.jpg",
"desc":"driving License",
"count":"2",
"field":"driving_license",
"visible":true
}
]
}
Model class below
struct Dynamic_obj : Codable {
var dt : String?
var url : String?
let desc : String?
var flag : Int?
let count : String?
let field : String?
let visible : Bool?
var bankname : String = "NA"
var pdfPassword : String = "NA"
var latitude : String = "NA"
var longitude : String = "NA"
var address : String = "NA"
enum CodingKeys: String, CodingKey {
case dt = "dt"
case url = "url"
case desc = "desc"
case flag = "flag"
case count = "count"
case field = "field"
case visible = "visible"
case bankname = "bankname"
case pdfPassword = "pdfPassword"
case latitude = "latitude"
case longitude = "longitude"
case address = "address"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
dt = try values.decodeIfPresent(String.self, forKey: .dt)
url = try values.decodeIfPresent(String.self, forKey: .url)
desc = try values.decodeIfPresent(String.self, forKey: .desc)
flag = try values.decodeIfPresent(Int.self, forKey: .flag)
count = try values.decodeIfPresent(String.self, forKey: .count)
field = try values.decodeIfPresent(String.self, forKey: .field)
visible = try values.decodeIfPresent(Bool.self, forKey: .visible)
bankname = try values.decodeIfPresent(String.self, forKey: .bankname) ?? "NA"
pdfPassword = try values.decodeIfPresent(String.self, forKey: .pdfPassword) ?? "NA"
latitude = try values.decodeIfPresent(String.self, forKey: .latitude) ?? "NA"
longitude = try values.decodeIfPresent(String.self, forKey: .longitude) ?? "NA"
address = try values.decodeIfPresent(String.self, forKey: .address) ?? "NA"
}
}
let decoder = JSONDecoder()
do {
let responseModel = try decoder.decode(LoanDocxPending.self, from: data)
if let mandotory = responseModel.mandatory{
self.visibleDocuments["Identity Proof-~id_proof"] = idProof
self.visibleTitle.append("Identity Proof")
}
} catch {
print("error")
}
struct LoanDocxPending :Codable {
let mandatory : Bool?
var dynamic_obj : [Dynamic_obj]?
enum CodingKeys: String, CodingKey {
case mandatory = "mandatory"
case dynamic_obj = "dynamic_obj"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
mandatory = try values.decodeIfPresent(Bool.self, forKey: .mandatory)
dynamic_obj = try values.decodeIfPresent([Dynamic_obj].self, forKey: .dynamic_obj)
}
}
First of all most of your code is not needed, for example the init methods and almost all CodingKeys.
Second of all as mentioned in the comments rather than printing meaningless literal "error" print the error instance. It will tell you that latitude and longitude are Double, not String.
This model matches the JSON in the question
struct DynamicObj : Codable {
let dt : String?
let url, desc : String
let flag : Int?
let count, field : String
let visible : Bool
var bankname, pdfPassword, address : String?
var latitude, longitude : Double?
}
struct LoanDocxPending :Codable {
let mandatory : Bool
var dynamicObj : [DynamicObj]
enum CodingKeys: String, CodingKey {
case mandatory,dynamicObj = "dynamic_obj"
}
}
let decoder = JSONDecoder()
do {
let responseModel = try decoder.decode(LoanDocxPending.self, from: data)
print(responseModel.mandatory)
} catch {
print("error", error)
}
I'm practicing my Swift skills, this time I'm trying to make a Covid-19 tracker, and for this I found this API, the thing is, that the format retrieved by /cases is something like this (changing keys to make it more readable)
{
"Country1": {
"All": {
"property1": 0,
"property2": "foo"
}
}, {
"All": {
"property1": "0",
"property2": "bar",
},
"State1": {
"property1": 0,
"property3": "foobar"
}
}
}
And I made the following structs to decode it:
Country
struct Country: Codable {
let All: [String: All]
private enum CodingKeys: String, CodingKey {
case All
}
}
All
struct All: Codable {
let confirmed: Int?
let recovered: Int?
let deaths: Int?
let country: String?
let population: Int?
let squareKmArea: Int?
let lifeExpectancy: String?
var elevationInMeters: String?
let continent: String?
let location: String?
let iso: String?
let capitalCity: String?
let lat: String?
let long: String?
let updated: String?
private enum CodingKeys: String, CodingKey {
case confirmed
case recovered
case deaths
case country
case population
case squareKmArea = "sq_km_area"
case lifeExpectancy = "life_expectancy"
case elevationInMeters = "elevation_in_meters"
case continent
case location
case iso
case capitalCity = "capital_city"
case lat
case long
case updated
}
init(confirmed: Int?, recovered: Int?, deaths: Int?, country: String?, population: Int?,
squareKmArea: Int?, lifeExpectancy: String?, elevationInMeters: String?,
continent: String?, location: String?, iso: String?, capitalCity: String?,
lat: String?, long: String?, updated: String?) {
self.confirmed = confirmed
self.recovered = recovered
self.deaths = deaths
self.country = country
self.population = population
self.squareKmArea = squareKmArea
self.lifeExpectancy = lifeExpectancy
self.elevationInMeters = elevationInMeters
self.continent = continent
self.location = location
self.iso = iso
self.capitalCity = capitalCity
self.lat = lat
self.long = long
self.updated = updated
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.confirmed = try container.decodeIfPresent(Int.self, forKey: .confirmed)
self.recovered = try container.decodeIfPresent(Int.self, forKey: .recovered)
self.deaths = try container.decodeIfPresent(Int.self, forKey: .deaths)
self.country = try container.decodeIfPresent(String.self, forKey: .country)
self.population = try container.decodeIfPresent(Int.self, forKey: .population)
self.squareKmArea = try container.decodeIfPresent(Int.self, forKey: .squareKmArea)
self.lifeExpectancy = try container.decodeIfPresent(String.self, forKey: .lifeExpectancy)
self.elevationInMeters = try container.decodeIfPresent(String.self, forKey: .elevationInMeters)
self.continent = try container.decodeIfPresent(String.self, forKey: .continent)
self.location = try container.decodeIfPresent(String.self, forKey: .location)
self.iso = try container.decodeIfPresent(String.self, forKey: .iso)
self.capitalCity = try container.decodeIfPresent(String.self, forKey: .capitalCity)
self.lat = try container.decodeIfPresent(String.self, forKey: .lat)
self.long = try container.decodeIfPresent(String.self, forKey: .long)
self.updated = try container.decodeIfPresent(String.self, forKey: .updated)
do {
self.elevationInMeters = try String(container.decodeIfPresent(Int.self, forKey: .elevationInMeters) ?? 0)
} catch DecodingError.typeMismatch {
print("Not a number")
self.elevationInMeters = try container.decodeIfPresent(String.self, forKey: .elevationInMeters) ?? ""
}
}
}
The elevation in meters can come in either String or Int value (4+ digits long > String (appends a comma), otherwise Int) (Idea taken from this answer
And I try to decode it this way
if let decodedData = self.jsonUtilities.decode(json: safeData, as: [String : Country].self) {
print(decodedData)
}
My above decode method looks like this:
func decode<T: Decodable>(json: Data, as clazz: T.Type) -> T? {
do {
let decoder = JSONDecoder()
let data = try decoder.decode(T.self, from: json)
return data
} catch {
print(error)
print("An error occurred while parsing JSON")
}
return nil
}
I found this answer where they suggest adding all the Coding Keys there, but these are a ton.
I haven't added all the keys, as it's an 8k+ lines JSON, so I was wondering if there's an easier way to decode it, as I can't think of a better way to decode this JSON with unique keys.
Alternatively if I could ignore all the keys that aren't "All" might also work, as I'm just trying to get the totals per country and their locations to place them in a map.
So far I get this error:
typeMismatch(Swift.Dictionary<Swift.String, Any>, Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Diamond Princess", intValue: nil), CodingKeys(stringValue: "All", intValue: nil), _JSONKey(stringValue: "recovered", intValue: nil)], debugDescription: "Expected to decode Dictionary<String, Any> but found a number instead.", underlyingError: nil))
An error occurred while parsing JSON
And afaik it's because it's not finding the key "Diamond Princess" which is a state (or so I believe) in Canada (according to my JSON), because I haven't added it for the reasons above.
When you have dynamic keys, you could decode it as [String: ...] dictionary.
The structure of the JSON is as follows:
{
"Canada": {
"All": { ... },
"Ontario": { ... },
...
},
"Mexico": { ... },
...
}
All the country stats have the key "All" and if that's all you need then you could create the following structs:
struct Country: Decodable {
var all: CountryAll
enum CodingKeys: String, CodingKey {
case all = "All"
}
}
struct CountryAll: Decodable {
var confirmed: Int
var recovered: Int
//.. etc
}
and decode as:
let countries = try JSONDecoder().decode([String: Country].self, from: json)
This question already has answers here:
How to parse JSON with Decodable protocol when property types might change from Int to String? [duplicate]
(4 answers)
Closed 3 years ago.
I am getting JSON data by using Codable method. Here, In my JSON response most of the values receiving as a String but sometime it received Int. on that time I am getting Decoding Error like below
TypeMismatch(Swift.String, Swift.DecodingError.Context(codingPath:
[CodingKeys(stringValue: "data", intValue: nil),
CodingKeys(stringValue: "status", intValue: nil)], debugDescription:
"Expected to decode String but found a number instead.",
underlyingError: nil))
Codable
// MARK: - Welcome
struct Welcome: Codable {
let status: Bool
let data: DataClass
}
// MARK: - DataClass
struct DataClass: Codable {
let memberID, groupID, memberName, dataDescription: String
let duedate, team, member: String
let membergroup: JSONNull?
let teams, category, junction, grouptype: String
let typegroup, groupMethod, memberCD, checklist: String
let refID, startDate, createdBy, createdDate: String
let startedBy: JSONNull?
let startedDate: String
let submittedBy, submittedDate, completedBy: JSONNull?
let completedDate, extendDate: String
let editedBy: JSONNull?
let editedDate: String
let status: Int
let autotask, isdelete, timestamp, assignedMember: String
let teamName, gemID, gemName, ticketName: String
let ticketTypeName, ableToDelete, assignedForMe, priorityFlag: String
let taskTypeFlag: String
let comments, notification, reminder: [JSONAny]
let acceptOrDecline: String
let availableStatus: [AvailableStatus]
let editedUserName: String
enum CodingKeys: String, CodingKey {
case memberID = "member_id"
case groupID = "group_id"
case memberName = "member_name"
case dataDescription = "description"
case duedate, team, member, membergroup, teams, category, junction, grouptype, typegroup
case groupMethod = "group_method"
case memberCD = "member_cd"
case checklist
case refID = "ref_id"
case startDate = "start_date"
case createdBy = "created_by"
case createdDate = "created_date"
case startedBy = "started_by"
case startedDate = "started_date"
case submittedBy = "submitted_by"
case submittedDate = "submitted_date"
case completedBy = "completed_by"
case completedDate = "completed_date"
case extendDate = "extend_date"
case editedBy = "edited_by"
case editedDate = "edited_date"
case status, autotask, isdelete, timestamp
case assignedMember = "assigned_member"
case teamName = "team_name"
case gemID = "gem_id"
case gemName = "gem_name"
case ticketName = "ticket_name"
case ticketTypeName = "ticket_type_name"
case ableToDelete = "able_to_delete"
case assignedForMe = "assigned_for_me"
case priorityFlag = "priority_flag"
case taskTypeFlag = "task_type_flag"
case comments, notification, reminder
case acceptOrDecline = "accept_or_decline"
case availableStatus = "available_status"
case editedUserName = "edited_user_name"
}
}
// MARK: - AvailableStatus
struct AvailableStatus: Codable {
let id: Int
let name, displayName, icon: String
enum CodingKeys: String, CodingKey {
case id, name
case displayName = "display_name"
case icon
}
}
you can do this:
// MARK: - Welcome
class Welcome: Codable {
let status: Bool
let data: DataClass
}
// MARK: - DataClass
class DataClass: Codable {
let memberID, groupID, memberName, dataDescription: String
let duedate, team, member: String
let membergroup: JSONNull?
let teams, category, junction, grouptype: String
let typegroup, groupMethod, memberCD, checklist: String
let refID, startDate, createdBy, createdDate: String
let startedBy: JSONNull?
let startedDate: String
let submittedBy, submittedDate, completedBy: JSONNull?
let completedDate, extendDate: String
let editedBy: JSONNull?
let editedDate: String
let status: Int
let autotask, isdelete, timestamp, assignedMember: String
let teamName, gemID, gemName, ticketName: String
let ticketTypeName, ableToDelete, assignedForMe, priorityFlag: String
let taskTypeFlag: String
let comments, notification, reminder: [JSONAny]
let acceptOrDecline: String
let availableStatus: [AvailableStatus]
let editedUserName: String
enum CodingKeys: String, CodingKey {
case memberID = "member_id"
case groupID = "group_id"
case memberName = "member_name"
case dataDescription = "description"
case duedate, team, member, membergroup, teams, category, junction, grouptype, typegroup
case groupMethod = "group_method"
case memberCD = "member_cd"
case checklist
case refID = "ref_id"
case startDate = "start_date"
case createdBy = "created_by"
case createdDate = "created_date"
case startedBy = "started_by"
case startedDate = "started_date"
case submittedBy = "submitted_by"
case submittedDate = "submitted_date"
case completedBy = "completed_by"
case completedDate = "completed_date"
case extendDate = "extend_date"
case editedBy = "edited_by"
case editedDate = "edited_date"
case status, autotask, isdelete, timestamp
case assignedMember = "assigned_member"
case teamName = "team_name"
case gemID = "gem_id"
case gemName = "gem_name"
case ticketName = "ticket_name"
case ticketTypeName = "ticket_type_name"
case ableToDelete = "able_to_delete"
case assignedForMe = "assigned_for_me"
case priorityFlag = "priority_flag"
case taskTypeFlag = "task_type_flag"
case comments, notification, reminder
case acceptOrDecline = "accept_or_decline"
case availableStatus = "available_status"
case editedUserName = "edited_user_name"
}
required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
memberID = try values.decodeIfPresent(String.self, forKey: .memberID)
groupID = try values.decodeIfPresent(String.self, forKey: .groupID)
//...... etc.
}
}
// MARK: - AvailableStatus
class AvailableStatus: Codable {
let id: Int
let name, displayName, icon: String
enum CodingKeys: String, CodingKey {
case id, name
case displayName = "display_name"
case icon
}
required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decodeIfPresent(Int.self, forKey: .id)
//...... etc.
}
}
Let's say I need to transform a date string I received from a web service to a Date object.
Using ObjectMapper, that was easy:
class Example: Mappable {
var date: Date?
required init?(map: Map) { }
func mapping(map: Map) {
date <- (map["date_of_interest"], GenericTransform().dateTransform)
}
}
I just had to implement a tranformer ("GenericTransform" in this case) for date, and pass it as an argument along with the key name to decode.
Now, using Codable:
class Example2: Codable {
var name: String
var age: Int
var date: Date?
enum CodingKeys: String, CodingKey {
case name, age
case date = "date_of_interest"
}
}
To transform a date, in my understanding, I'd have to either:
1) Pass a dateDecodingStrategy to my JSONDecoder, which I don't want to, because I'm trying to keep that part of the code as a generic function.
or
2) Implement an init(from decoder: Decoder) inside Example2, which I also don't want to, because of the boilerplate code I'd have to write to decode all the other properties (which would be automatically generated otherwise):
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
age = try container.decode(Int.self, forKey: .age)
let dateString = try container.decode(String.self, forKey: .date)
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
if let date = formatter.date(from: dateString) {
self.date = date
} else {
//throw error
}
}
My question is: is there an easier way to do it than options 1 and 2 above?
Maybe tweaking the CodingKeys enum somehow?
EDIT:
The problem is not only about dates, actually. In this project that I'm working on, there are many custom transformations being done using TransformOf<ObjectType, JSONType> from ObjectMapper.
For example, a color transformation of a hex code received from a web service into a UIColor is done using this bit of code:
let colorTransform = TransformOf<UIColor, String>(fromJSON: { (value) -> UIColor? in
if let value = value {
return UIColor().hexStringToUIColor(hex: value)
}
return nil
}, toJSON: { _ in
return nil
})
I'm trying to remove ObjectMapper from the project, making these same transformations using Codable, so only using a custom dateDecodingStrategy will not suffice.
How would you guys do it? Implement a custom init(from decoder: Decoder) for every class that has to decode, for example, a color hex code?
Using dateDecodingStrategy in your case (as you only reference a single date format) is very simple…
let decoder = JSONDecoder()
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
decoder.dateDecodingStrategy = .formatted(formatter)
We can use custom method for example like decodeAll here. Try in playground.
struct Model: Codable {
var age: Int?
var name: String?
enum CodingKeys: String, CodingKey {
case name
case age
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decodeAll(String.self, forKey: .name)
age = try container.decodeAll(Int.self, forKey: .age)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try? container.encode(name, forKey: .name)
try? container.encode(age, forKey: .age)
}
}
extension KeyedDecodingContainer where K: CodingKey, K: CustomDebugStringConvertible {
func decodeAll<T: Decodable>(_ type: T.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> T {
if let obj = try? decode(T.self, forKey: key) {
return obj
} else {
if type == String.self {
if let obj = try? decode(Int.self, forKey: key), let val = String(obj) as? T {
return val
} else if let obj = try? decode(Double.self, forKey: key), let val = String(obj) as? T {
return val
}
} else if type == Int.self {
if let obj = try? decode(String.self, forKey: key), let val = Int(obj) as? T {
return val
} else if let obj = try? decode(Double.self, forKey: key), let val = Int(obj) as? T {
return val
}
} else if type == Double.self {
if let obj = try? decode(String.self, forKey: key), let val = Double(obj) as? T {
return val
} else if let obj = try? decode(Int.self, forKey: key), let val = Double(obj) as? T {
return val
}
}
}
throw DecodingError.typeMismatch(T.self, DecodingError.Context(codingPath: codingPath, debugDescription: "Wrong type for: \(key.stringValue)"))
}
}
let json = ##"{ "age": "5", "name": 98 }"##
do {
let obj = try JSONDecoder().decode(Model.self, from: json.data(using: .utf8)!)
print(obj)
} catch {
print(error)
}
I'm going through some projects and removing JSON parsing frameworks, as it seems pretty simple to do with Swift 4. I've encountered this oddball JSON return where Ints and Dates are returned as Strings.
I looked at GrokSwift's Parsing JSON with Swift 4, Apple's website, but I don't see anything that jumps out re: changing types.
Apple's example code shows how to change key names, but I'm having a hard time figuring out how to change the key type.
Here's what it looks like:
{
"WaitTimes": [
{
"CheckpointIndex": "1",
"WaitTime": "1",
"Created_Datetime": "10/17/2017 6:57:29 PM"
},
{
"CheckpointIndex": "2",
"WaitTime": "6",
"Created_Datetime": "10/12/2017 12:28:47 PM"
},
{
"CheckpointIndex": "0",
"WaitTime": "8",
"Created_Datetime": "9/26/2017 5:04:42 AM"
}
]
}
I've used CodingKey to rename dictionary keys to a Swift-conforming entry, as follows:
struct WaitTimeContainer: Codable {
let waitTimes: [WaitTime]
private enum CodingKeys: String, CodingKey {
case waitTimes = "WaitTimes"
}
struct WaitTime: Codable {
let checkpointIndex: String
let waitTime: String
let createdDateTime: String
private enum CodingKeys: String, CodingKey {
case checkpointIndex = "CheckpointIndex"
case waitTime = "WaitTime"
case createdDateTime = "Created_Datetime"
}
}
}
That still leaves me with String that should be Int or Date. How would I go about converting a JSON return that contains an Int/Date/Float as a String to an Int/Date/Float using the Codable protocol?
This is not yet possible as Swift team has provided only String to date decoder in JSONDecoder.
You can always decode manually though:
struct WaitTimeContainer: Decodable {
let waitTimes: [WaitTime]
private enum CodingKeys: String, CodingKey {
case waitTimes = "WaitTimes"
}
struct WaitTime:Decodable {
let checkpointIndex: Int
let waitTime: Float
let createdDateTime: Date
init(checkpointIndex: Int, waitTime: Float, createdDateTime:Date) {
self.checkpointIndex = checkpointIndex
self.waitTime = waitTime
self.createdDateTime = createdDateTime
}
static let formatter: DateFormatter = {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .iso8601)
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "MM/dd/yyyy hh:mm:ss a"
return formatter
}()
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let checkpointIndexString = try container.decode(String.self, forKey: .checkpointIndex)
let checkpointIndex = Int(checkpointIndexString)!
let waitTimeString = try container.decode(String.self, forKey: .waitTime)
let waitTime = Float(waitTimeString)!
let createdDateTimeString = try container.decode(String.self, forKey: .createdDateTime)
let createdDateTime = WaitTime.formatter.date(from: createdDateTimeString)!
self.init(checkpointIndex:checkpointIndex, waitTime:waitTime, createdDateTime:createdDateTime)
}
private enum CodingKeys: String, CodingKey {
case checkpointIndex = "CheckpointIndex"
case waitTime = "WaitTime"
case createdDateTime = "Created_Datetime"
}
}
}
public extension KeyedDecodingContainer {
public func decode(_ type: Date.Type, forKey key: Key) throws -> Date {
let dateString = try self.decode(String.self, forKey: key)
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MM/dd/yyyy hh:mm:ss a"
guard let date = dateFormatter.date(from: dateString) else {
let context = DecodingError.Context(codingPath: codingPath,
debugDescription: "Could not parse json key to a Date")
throw DecodingError.dataCorrupted(context)
}
return date
}
}
Usage: -
let date: Date = try container.decode(Date.self, forKey: . createdDateTime)
Let me give suggest two approaches: one for dealing with String backed values and another - for dealing with dates that might come in different formats. Hope the example is self-explantory.
import Foundation
protocol StringRepresentable: CustomStringConvertible {
init?(_ string: String)
}
extension Int: StringRepresentable {}
extension Double: StringRepresentable {}
struct StringBacked<Value: StringRepresentable>: Codable, CustomStringConvertible {
var value: Value
var description: String {
value.description
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let string = try container.decode(String.self)
guard let value = Value(string) else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: """
Failed to convert an instance of \(Value.self) from "\(string)"
"""
)
}
self.value = value
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(value.description)
}
}
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in
let container = try decoder.singleValueContainer()
let dateStr = try container.decode(String.self)
let formatters = [
"yyyy-MM-dd",
"yyyy-MM-dd'T'HH:mm:ssZZZZZ",
"yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ",
"yyyy-MM-dd'T'HH:mm:ss'Z'",
"yyyy-MM-dd'T'HH:mm:ss.SSS",
"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
"yyyy-MM-dd HH:mm:ss",
"MM/dd/yyyy HH:mm:ss",
"MM/dd/yyyy hh:mm:ss a"
].map { (format: String) -> DateFormatter in
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = format
return formatter
}
for formatter in formatters {
if let date = formatter.date(from: dateStr) {
return date
}
}
throw DecodingError.valueNotFound(String.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Could not parse json key: \(container.codingPath), value: \(dateStr) into a Date"))
})
// Test it with data:
let jsonData = """
{
"WaitTimes": [
{
"CheckpointIndex": "1",
"WaitTime": "1",
"Created_Datetime": "10/17/2017 6:57:29 PM"
},
{
"CheckpointIndex": "2",
"WaitTime": "6",
"Created_Datetime": "10/12/2017 12:28:47 PM"
},
{
"CheckpointIndex": "0",
"WaitTime": "8",
"Created_Datetime": "9/26/2017 5:04:42 AM"
}
]
}
""".data(using: .utf8)!
struct WaitTimeContainer: Codable {
let waitTimes: [WaitTime]
private enum CodingKeys: String, CodingKey {
case waitTimes = "WaitTimes"
}
struct WaitTime: Codable {
var checkpointIndex: Int {
get { return checkpointIndexString.value }
set { checkpointIndexString.value = newValue }
}
var waitTime: Double {
get { return waitTimeString.value }
set { waitTimeString.value = newValue }
}
let createdDateTime: Date
private var checkpointIndexString: StringBacked<Int>
private var waitTimeString: StringBacked<Double>
private enum CodingKeys: String, CodingKey {
case checkpointIndexString = "CheckpointIndex"
case waitTimeString = "WaitTime"
case createdDateTime = "Created_Datetime"
}
}
}
let waitTimeContainer = try decoder.decode(WaitTimeContainer.self, from: jsonData)
print(waitTimeContainer)