Codable - Applying Data Transformations
Codable may not just all be blindly transforming JSON data into model objects, but you can also apply formatting to it. Examples of formatters include, but not limited to date formatters, URLs, and fractional data types.
In this post we're going to take a look at some of those formatters and see how to use them.
- URL formatters
Codable allows for automatic String to URL transformation. The way it is set up, we don't even have to apply any manual logic to do it. Let's take a look at an example. Let's assume this is how our input JSON looks like,
let json = """
{"name": "Apple", "url": "www.apple.com"}
"""
This is a very simple JSON, so our tiny model looks like this,
struct Company: Decodable {
let name: String
let url: URL
}
Now as this bullet point indicates, Codable will take care of automatic conversion from String
in JSON to the url
property of type URL
on Company
model . All we need to do is to ask Codable
to perform that transform.
do {
let company = try JSONDecoder().decode(Company.self, from: json.data(using: .utf8)!)
// Prints
// Company(name: "Apple", url: www.apple.com)
//
print(company)
// po company.url prints
// www.apple.com
// - _url : www.apple.com
} catch {
print(error.localizedDescription)
}
2. Date formatter
In this section we are going to take a look at how we can use custom date formatters to automatically convert incoming date string in JSON to internal iOS Date
object.
Let's assume that this is how our incoming JSON looks like,
let json = """
{"name": "Apple", "url": "www.apple.com", "foundationDate": "04/01/1975"}
"""
We will reuse, but extend the earlier used Company
struct to include one more parameter, foundationDate
struct Company: Decodable {
let name: String
let url: URL
let foundationDate: Date
}
Since this change involves String
to Date
conversion using custom DateFormatters
, we will also declare our date formatter with known date format on the top.
// You can change DateFormatter as per requirements. More date formatters can be found at
// https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/DataFormatting
lazy var dateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MM/dd/yyyy"
return dateFormatter
}()
Please note that adateFormatter
is declare aslazy
. Meaning it gets initialized only on the first access and for subsequent uses, we use the previously initialized value. This is done for the performance reasons, since creatingDateFormatter
is slow and causes a significant performance hit
And we will use this dateFormatter
when it comes to deciding dateDecodingStrategy
for our decoder object.
do {
let decoder = JSONDecoder()
// This is the crucial step to assign date formatter to our decoder
decoder.dateDecodingStrategy = .formatted(dateFormatter)
let decodedDictionary = try decoder.decode(Company.self, from: json.data(using: .utf8)!)
let calendar = Calendar.current
// Prints 4
print("Month \(calendar.component(.month, from: decodedDictionary.foundationDate))")
// Prints 1
print("Day \(calendar.component(.day, from: decodedDictionary.foundationDate))")
// Prints 1975
print("Year \(calendar.component(.year, from: decodedDictionary.foundationDate))")
} catch {
print(error.localizedDescription)
}
3. iso Date Formatter
Earlier we took a look at plane date strings. Now is the time to do some more action dealing with iso formatted dates.
let json = """
{"name": "Apple", "url": "www.apple.com", "isoFormattedDate": "1975-01-24T21:30:31Z"}
"""
We will slightly alter our Company
model for this purpose,
struct Company: Decodable {
let name: String
let url: URL
let isoFormattedDate: Date
}
Since this is a different date format, we will go ahead and introduce newer date formatter conforming to new date that we're getting back from the endpoint,
lazy var iso8601DateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
return dateFormatter
}()
Once we have everything - Model, JSON and custom date formatter now is the time to go ahead and start decoding incoming JSON
do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(iso8601DateFormatter)
let decodedDictionary = try decoder.decode(Company.self, from: json.data(using: .utf8)!)
let calendar = Calendar.current
// Prints Month 1
print("Month \(calendar.component(.month, from: decodedDictionary.isoFormattedDate))")
// Prints Day 24
print("Day \(calendar.component(.day, from: decodedDictionary.isoFormattedDate))")
// Prints Year 1975
print("Year \(calendar.component(.year, from: decodedDictionary.isoFormattedDate))")
// Prints Hour 21
print("Hour \(calendar.component(.hour, from: decodedDictionary.isoFormattedDate))")
// Prints Minute 30
print("Minute \(calendar.component(.minute, from: decodedDictionary.isoFormattedDate))")
// Prints second 31
print("Second \(calendar.component(.second, from: decodedDictionary.isoFormattedDate))")
} catch {
print(error.localizedDescription)
}
4. Building your custom decoder
Codable also makes it easy to build your custom formatter if you want to apply additional mapping to values you get back from server.
let json = """
{"firstName": "Apple", "lastName": "Corporation", "marketValue": "100000", "numberOfEmployees": "6000"}
"""
Unfortunately, this is not the data we directly want to use in our app. To mitigate this issue, we will implement our custom decoder. Below are the things we want to achieve by building custom formatter,
- To convert first and last name into full name
- Convert market value in localized currency string format
- Convert number of employees from string to int format
Here's the full implementation of struct
company to be able to apply all these custom conversions,
struct Company: Decodable {
let fullName: String
let formattedMarketCap: String?
let numberOfEmployees: Int?
static let currencyFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.locale = Locale.current
return formatter
}()
enum CodingKeys: String, CodingKey {
case firstName
case lastName
case marketValue
case numberOfEmployees
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let firstName = try values.decode(String.self, forKey: .firstName)
let lastName = try values.decode(String.self, forKey: .lastName)
self.fullName = "\(firstName) \(lastName)"
let marketValue = try values.decode(Double.self, forKey: .marketValue)
if let marketCapString = Company.currencyFormatter.string(from: NSNumber(value: marketValue)) {
self.formattedMarketCap = marketCapString
} else {
self.formattedMarketCap = nil
}
let numberOfEmployees = try values.decode(String.self, forKey: .numberOfEmployees)
self.numberOfEmployees = Int(numberOfEmployees)
}
}
This is a very simple example as long as these mappings are concerned, but it explains how you can utilize the power of custom decoders to apply any transformations you want
Once we have our data model and applicable transformation rules, now is the time to go ahead and convert it into respective Codable
object
let companyDetails = """
{"firstName": "Apple", "lastName": "Corporation", "marketValue": 100000.11, "numberOfEmployees": "6000"}
"""
do {
let decoder = JSONDecoder()
let company = try decoder.decode(Company.self, from: companyDetails.data(using: .utf8)!)
// Prints Full name Apple Corporation
print("Full name \(company.fullName)")
// Prints Formatted Market Cap $100,000.11
print("Formatted Market Cap \(company.formattedMarketCap ?? "")")
// Prints Number of Employees 6000
print("Number of Employees \(company.numberOfEmployees ?? 0)")
} catch {
print(error.localizedDescription)
}
This is all for now. If you come across additional date formatters or want me to write about some more date formatters that I wasn't aware, feel free to leave comments.
Thanks for reading, and I hope you all have a great Thanksgiving!
References: