There are great libraries out there that help you parse JSON data, but it is great to see how Swift 5 implements a fully-native solution to make your data types encodable and decodable for compatibility with JSON.
There are three new protocols in the Swift standard library that define an standardized approach to data encoding and decoding. These new protocols are Encodable
, Decodable
and Codable
Encoding and Decoding
The easiest way to encode or decode a type is to use standard properties that are already Codable
, which can be types like String
, Int
, Double
, Data
, Date
and URL
.
Let’s consider a User
structure that stores the name, the username and the phone number of a user.
struct User { | |
var name: String | |
var username: String | |
var phoneNumber: String | |
} |
We can make it automatically decodable and encodable by conforming to the Codable
protocol, which is a type alias of Encodable and Decodable.
struct User: Codable { | |
var name: String | |
var username: String | |
var phoneNumber: String | |
} |
We can also use custom types that also conform to the Codable
protocol, and use built-in types such as Array
, Dictionary
and Optional
whenever they contain codable types.
Let’s add to the previous example a list of devices that the user owns. It is represented by an Array
of Device
. The Device
stores the name and the manufacturer of a device.
struct Device: Codable { | |
var name: String | |
var manufacturer: String | |
} | |
struct User: Codable { | |
var name: String | |
var username: String | |
var phoneNumber: String | |
var devices: [Device] | |
} |
We can now deserialize a JSON document into our User
instance, and serialize that instance back into a JSON document.
func decode(data: Data) throws -> User? { | |
do { | |
let decoder = JSONDecoder() | |
let user = try decoder.decode(User.self, from: data) | |
return user | |
} catch let error { | |
print(error) | |
return nil | |
} | |
} | |
func encode(user: User) -> Data? { | |
do { | |
let encoder = JSONEncoder() | |
let data = try encoder.encode(user) | |
return data | |
} catch let error { | |
print(error) | |
return nil | |
} | |
} | |
func loadUser() -> User? { | |
guard let fileURL = Bundle.main.url(forResource: "user", withExtension: "json") else { | |
print("couldn't find the file") | |
return nil | |
} | |
do { | |
let content = try Data(contentsOf: fileURL) | |
let user = try decode(data: content) | |
return user | |
} catch let error { | |
print(error) | |
return nil | |
} | |
} | |
// We can decode a User from a json document | |
if let user = loadUser() { | |
print(user.name) | |
for device in user.devices { | |
print(device.name) | |
} | |
// We can encode the user | |
if let data = encode(user: user) { | |
print(data) | |
} | |
} |
Encode or Decode Exclusively
In some cases, you may not need to decode and encode the same type. For example, you may only need to read data and decode them into an instance, or you just need to make API calls and there is no need to decode the response containing the same type.
You can achieve that by conforming Encodable
and Decodable
protocols. The example below shows how the User
structure only encode data:
struct Device: Codable { | |
var name: String | |
var manufacturer: String | |
} | |
struct User: Encodable { | |
var name: String | |
var username: String | |
var phoneNumber: String | |
var devices: [Device] | |
} | |
func encode(user: User) -> Data? { | |
do { | |
let encoder = JSONEncoder() | |
let data = try encoder.encode(user) | |
return data | |
} catch let error { | |
print(error) | |
return nil | |
} | |
} | |
let device = Device(name: "iPhone 8 Plus", manufacturer: "Apple") | |
let user = User(name: "Mike Ross", username: "mross", phoneNumber: "555-523-234", devices: [device]) | |
if let data = encode(user: user) { | |
print(data) | |
} |
Custom Key Names
It is often the case that we parse JSON documents with properties that doesn’t match our property names. In that case, we need to define custom key names in order to parse these properties into our instances.
In order to define the custom list of key names we need to define an enumeration named CodingKeys
which conforms to the CodingKeys
protocol. The names of the enumeration cases should match the names you’ve given to the corresponding properties in your type.
Let’s update the property phoneNumber
of our User
structure, which is defined in the JSON document using snake-case style. The enumeration CodingKeys
provide the phoneNumber
alternative key specified by a String
as the raw-value type for the CodingKeys
enumeration.
struct Device: Codable { | |
var name: String | |
var manufacturer: String | |
} | |
struct User: Encodable { | |
var name: String | |
var username: String | |
var phoneNumber: String | |
var devices: [Device] | |
enum CodingKeys: String, CodingKey { | |
case name | |
case username | |
case phoneNumber = "phone_number" | |
case devices | |
} | |
} |
If you find this post helpful, please recommend it for others to read.
Top comments (0)