June 07, 2017 - Swift 4.0
Swift 4 includes a new way to generate & parse JSON using the Codable
protocol. It’ll get rid of some boilerplate, especially when the objects or structs in our code have a similar structure to the JSON that we use to talk to a web service. In many cases you’ll be able to avoid writing any code that explicitly parses or generates JSON, even if your Swift structs don’t exactly match the JSON structure. That means no more long, ugly toJSON()
or init?(json: [String: Any])
functions.
Codable
can also replace use of NSCoding
when we want to serialize objects to write them to a file and read them back again. It can work with plists as easily as JSON and you can write your own custom encoders & decoders for different formats.
Today let’s look at the simple case of converting an object or struct in our code to & from JSON. Then we’ll see how it might be more complicated when the JSON doesn’t match the objects & structs that we’re using in our code. We’ll use structs below but everything is the same if you need to use classes.
This tutorial uses Swift 4.0. The easiest way to use Swift 4 is to download the Xcode 9 beta. You can install it next to Xcode 8.
Like in previous tutorials, we’ll use the super handy JSONPlaceholder API:
“JSONPlaceholder is a fake online REST API for testing and prototyping. It’s like image placeholders but for web developers.”
JSONPlaceholder has a handful of resources similar to what you’ll find in a lot of apps: users, posts, photos, albums, … We’ll use Todo and User items today.
The JSON structure of a Todo looks like:
{
"userId": 1,
"id": 1,
"title": "delectus aut autem",
"completed": false
}
So let’s create a struct to represent that:
struct Todo: Codable {
var title: String
var id: Int?
var userId: Int
var completed: Int
}
When we create a Todo
in the app it won’t have an id
until it’s saved to the server so that property is optional.
Convert a Single Item to JSON
Before we had Codable
, we’d have to parse the JSON ourselves. Let’s looks at how that worked.
For fetching objects that could be done by creating an initializer that tries to create an object from JSON:
struct Todo {
// ...
init?(json: [String: Any]) {
guard let title = json["title"] as? String,
let id = json["id"] as? Int,
let userId = json["userId"] as? Int,
let completed = json["completed"] as? Int else {
return nil
}
self.title = title
self.userId = userId
self.completed = completed
self.id = id
}
}
For convenience, let’s also make a function that will give us the URL to hit for this call based on the id
of the Todo
that we want to retrieve:
struct Todo {
// ...
static func endpointForID(_ id: Int) -> String {
return "https://jsonplaceholder.typicode.com/todos/\(id)"
}
}
And define some error cases so we can return something informative if we run into issues:
enum BackendError: Error {
case urlError(reason: String)
case objectSerialization(reason: String)
}
Then we can use URLSession
to make that API call, check for errors, parse the JSON, and return the Todo
:
static func todoByID(_ id: Int, completionHandler: @escaping (Todo?, Error?) -> Void) {
// set up URLRequest with URL
let endpoint = Todo.endpointForID(id)
guard let url = URL(string: endpoint) else {
print("Error: cannot create URL")
let error = BackendError.urlError(reason: "Could not construct URL")
completionHandler(nil, error)
return
}
let urlRequest = URLRequest(url: url)
// Make request
let session = URLSession.shared
let task = session.dataTask(with: urlRequest, completionHandler: {
(data, response, error) in
// handle response to request
// check for error
guard error == nil else {
completionHandler(nil, error!)
return
}
// make sure we got data in the response
guard let responseData = data else {
print("Error: did not receive data")
let error = BackendError.objectSerialization(reason: "No data in response")
completionHandler(nil, error)
return
}
// parse the result as JSON
// then create a Todo from the JSON
do {
if let todoJSON = try JSONSerialization.jsonObject(with: responseData, options: []) as? [String: Any],
let todo = Todo(json: todoJSON) {
// created a TODO object
completionHandler(todo, nil)
} else {
// couldn't create a todo object from the JSON
let error = BackendError.objectSerialization(reason: "Couldn't create a todo object from the JSON")
completionHandler(nil, error)
}
} catch {
// error trying to convert the data to JSON using JSONSerialization.jsonObject
completionHandler(nil, error)
return
}
})
task.resume()
}
To call that function and check the results:
func getTodo(_ idNumber: Int) {
Todo.todoByIDOld(idNumber, completionHandler: { (todo, error) in
if let error = error {
// got an error in getting the data, need to handle it
print(error)
return
}
guard let todo = todo else {
print("error getting first todo: result is nil")
return
}
// success :)
debugPrint(todo)
print(todo.title)
})
}
Now, what would that look like in Swift 4? We still need to create the URL and make sure we got data (and no error) from the network call. We’ll can replace this chunk of code with something more concise and delete that JSON-based initializer:
do {
if let todoJSON = try JSONSerialization.jsonObject(with: responseData, options: []) as? [String: Any],
let todo = Todo(json: todoJSON) {
// created a TODO object
completionHandler(todo, nil)
} else {
// couldn't create a todo object from the JSON
let error = BackendError.objectSerialization(reason: "Couldn't create a todo object from the JSON")
completionHandler(nil, error)
}
} catch {
// error trying to convert the data to JSON using JSONSerialization.jsonObject
completionHandler(nil, error)
return
}
Instead of using JSONSerialization.jsonObject
then our initializer: let todo = Todo(json: todoJSON)
, we’ll use the Codable
protocol to do it in one step.
First, we need to declare that Todo
is Codable
:
struct Todo: Codable {
// ...
}
Then we can change that JSON parsing code:
static func todoByID(_ id: Int, completionHandler: @escaping (Todo?, Error?) -> Void) {
// ...
let task = session.dataTask(with: urlRequest, completionHandler: {
(data, response, error) in
guard let responseData = data else {
// ...
}
guard error == nil else {
// ...
}
let decoder = JSONDecoder()
do {
let todo = try decoder.decode(Todo.self, from: responseData)
completionHandler(todo, nil)
} catch {
print("error trying to convert data to JSON")
print(error)
completionHandler(nil, error)
}
})
task.resume()
}
Here’s the new stuff:
let decoder = JSONDecoder()
do {
let todo = try decoder.decode(Todo.self, from: responseData)
completionHandler(todo, nil)
} catch {
print("error trying to convert data to JSON")
print(error)
completionHandler(nil, error)
}
First we create a JSONDecoder()
. Then we try to use it to decode the data that we got from the network call. Finally, if it succeeds we hand back the Todo
to the caller using the completion handler. If it fails then we hand back an error instead.
To use the decoder we need to tell it what the class is: Todo.self
and what data to parse:
let todo = try decoder.decode(Todo.self, from: responseData)
Which is a little nicer than the previous version:
let todoJSON = try JSONSerialization.jsonObject(with: responseData, options: []) as? [String: Any]
let todo = Todo(json: todoJSON)
But also meant that we didn’t have to write an initializer that creates a Todo
from JSON. Extracting all of the elements from the JSON and setting the properties of the Todo was done for us. We can now delete the init?(json: [String: Any])
function.
Convert an Array of Items to JSON
Similarly, we can easily fetch all of the Todos from https://jsonplaceholder.typicode.com/todos.
Add the URL string to we can refer to it:
struct Todo: Codable {
// ...
static func endpointForTodos() -> String {
return "https://jsonplaceholder.typicode.com/todos/"
}
}
And create a new function that’s similar to todoById(_:)
:
static func allTodos(completionHandler: @escaping ([Todo]?, Error?) -> Void) {
let endpoint = Todo.endpointForTodos()
guard let url = URL(string: endpoint) else {
print("Error: cannot create URL")
let error = BackendError.urlError(reason: "Could not construct URL")
completionHandler(nil, error)
return
}
let urlRequest = URLRequest(url: url)
let session = URLSession.shared
let task = session.dataTask(with: urlRequest) {
(data, response, error) in
guard let responseData = data else {
print("Error: did not receive data")
completionHandler(nil, error)
return
}
guard error == nil else {
completionHandler(nil, error)
return
}
let decoder = JSONDecoder()
do {
let todos = try decoder.decode([Todo].self, from: responseData)
completionHandler(todos, nil)
} catch {
print("error trying to convert data to JSON")
print(error)
completionHandler(nil, error)
}
}
task.resume()
}
The JSON handling is almost the same as the previous function:
let decoder = JSONDecoder()
do {
let todos = try decoder.decode([Todo].self, from: responseData)
completionHandler(todos, nil)
} catch {
print("error trying to convert data to JSON")
print(error)
completionHandler(nil, error)
}
The difference is the type passed to decoder.decode
. Instead of a single Todo
it’s an array of Todos: [Todo].self
.
Calling that function is pretty similar too, except you’ll get an array of Todo
items in the completion handler instead of just one:
func getAllTodos() {
Todo.allTodos { (todos, error) in
if let error = error {
// got an error in getting the data
print(error)
return
}
guard let todos = todos else {
print("error getting all todos: result is nil")
return
}
// success :)
debugPrint(todos)
print(todos.count)
}
}
That’s because the completion handler is declared like completionHandler: @escaping ([Todo]?, Error?) -> Void
.
Generate JSON from an Item
The Codable
protocol is made up of 2 protocols: Encodable
as well as Decodable
. So far we’ve only used Decodable
to parse JSON. Encodable
can make it easier to generate JSON too.
Here’s an example of saving a Todo to the web service without using Codable
:
func save(completionHandler: @escaping (Error?) -> Void) {
let todosEndpoint = Todo.endpointForTodos()
guard let todosURL = URL(string: todosEndpoint) else {
let error = BackendError.urlError(reason: "Could not create URL")
completionHandler(error)
return
}
var todosUrlRequest = URLRequest(url: todosURL)
todosUrlRequest.httpMethod = "POST"
let newTodoAsJSON = self.toJSON()
do {
let jsonTodo = try JSONSerialization.data(withJSONObject: newTodoAsJSON, options: [])
todosUrlRequest.httpBody = jsonTodo
} catch {
let error = BackendError.objectSerialization(reason: "Could not create JSON from Todo")
completionHandler(error)
return
}
let session = URLSession.shared
let task = session.dataTask(with: todosUrlRequest, completionHandler: {
(data, response, error) in
guard error == nil else {
completionHandler(error!)
return
}
// if we didn't get an error then it succeeded
completionHandler(nil)
})
task.resume()
}
To make that work we need to add a function to convert the Todo
to JSON:
struct Todo {
// ...
func toJSON() -> [String: Any] {
var json = [String: Any]()
json["title"] = title
json["userId"] = userId
json["completed"] = completed
if let id = id {
json["id"] = id
}
return json
}
}
Now in Swift 4 we can avoid writing functions like toJSON()
by using an JSONEncoder
instead of JSONSerialization
:
func save(completionHandler: @escaping (Error?) -> Void) {
// ...
var todosUrlRequest = URLRequest(url: todosURL)
todosUrlRequest.httpMethod = "POST"
let encoder = JSONEncoder()
do {
let newTodoAsJSON = try encoder.encode(self)
todosUrlRequest.httpBody = newTodoAsJSON
} catch {
print(error)
completionHandler(error)
}
// ...
let task = session.dataTask(with: todosUrlRequest, completionHandler: {
(data, response, error) in
// ...
})
task.resume()
}
The old version isn’t much longer to call:
let newTodoAsJSON = try JSONSerialization.data(withJSONObject: newTodoAsJSON, options: [])
Compared to:
let newTodoAsJSON = try encoder.encode(self)
But being able to delete the toJSON()
function is nice.
Nested JSON Objects
What if your JSON includes objects inside of objects? For example, a User
from the JSON Placeholder API has an address and a company within it:
{
"id": 1,
"name": "Leanne Graham",
"username": "Bret",
"email": "Sincere@april.biz",
"address": {
"street": "Kulas Light",
"suite": "Apt. 556",
"city": "Gwenborough",
"zipcode": "92998-3874",
"geo": {
"lat": "-37.3159",
"lng": "81.1496"
}
},
"phone": "1-770-736-8031 x56442",
"website": "hildegard.org",
"company": {
"name": "Romaguera-Crona",
"catchPhrase": "Multi-layered client-server neural-net",
"bs": "harness real-time e-markets"
}
}
Let’s try to parse a few fields from the user fields and a few from inside the address object. First we’ll create Codable
structs for both User
and Address
:
struct Address: Codable {
let city: String
let zipcode: String
}
struct User: Codable {
let id: Int?
let name: String
let email: String
let address: Address
// MARK: URLs
static func endpointForID(_ id: Int) -> String {
return "https://jsonplaceholder.typicode.com/users/\(id)"
}
}
Then we’ll set up a function to get a user:
static func userByID(_ id: Int, completionHandler: @escaping (User?, Error?) -> Void) {
let endpoint = User.endpointForID(id)
guard let url = URL(string: endpoint) else {
print("Error: cannot create URL")
let error = BackendError.urlError(reason: "Could not create URL")
completionHandler(nil, error)
return
}
let urlRequest = URLRequest(url: url)
let session = URLSession.shared
let task = session.dataTask(with: urlRequest, completionHandler: {
(data, response, error) in
guard let responseData = data else {
print("Error: did not receive data")
completionHandler(nil, error)
return
}
guard error == nil else {
completionHandler(nil, error!)
return
}
let decoder = JSONDecoder()
do {
let user = try decoder.decode(User.self, from: responseData)
completionHandler(user, nil)
} catch {
print("error trying to convert data to JSON")
print(error)
completionHandler(nil, error)
}
})
task.resume()
}
The bit that does the JSON encoding is:
let user = try decoder.decode(User.self, from: responseData)
Then we can call that function and try to print out the user and address:
func getUser(_ idNumber: Int) {
User.userByID(idNumber, completionHandler: { (user, error) in
if let error = error {
// got an error in getting the data, need to handle it
print("error calling POST on /todos")
print(error)
return
}
guard let user = user else {
print("error getting user: result is nil")
return
}
// success :)
debugPrint(user)
print(user.name)
print(user.address.city)
})
}
It works! Here’s what that prints out:
grok101.User(id: Optional(1), name: "Leanne Graham", email: "Sincere@april.biz", address: grok101.Address(city: "Gwenborough", zipcode: "92998-3874"))
Leanne Graham
Gwenborough
That example shows us two things:
To parse nested JSON just create properties with
Codable
items for the nested JSON objectsWe don’t have to parse every element in the JSON. We can pick and choose just the elements that we want to parse by only creating properties for those items in our
Codable
item.
What if the JSON Doesn’t Match My Struct / Object?
Codable
works almost magically when the JSON matches the object. Sometimes the JSON won’t exactly match your Swift declaration. So how can we handle that?
Different Property Names
The simplest case is that the property names don’t match. Suppose we wanted to use a more verbose name for the id
property like serverId
:
struct Todo: Codable {
var title: String
var serverId: Int?
var userId: Int
var completed: Int
}
Now if we try to run the todoById(:_)
function it’ll work but we’ll end up without a value for the serverId
:
Todo(title: "delectus aut autem", serverId: nil, userId: 1, completed: 0)
Since serverId
is optional the parsing won’t fail but it won’t be right either. Fortunately the Codable
protocol lets us specify the json key that should match up with each property using enum CodingKeys
:
enum CodingKeys: String, CodingKey {
case title
case serverId = "id"
case userId
case completed
}
In CodingKeys
we include all of the keys to be parsed from the JSON. If the name of the property matches the json key then we only need to include case propertyName
. If they don’t match then we need to include both the property name and the json key: case propertyName = "json_key"
. If they all match up then the default version of CodingKeys
will be automatically created and we don’t need to add it in our code.
Using case serverId = "id"
will fix our JSON parsing so that the serverId
gets correctly parsed:
Todo(title: "delectus aut autem", serverId: Optional(1), userId: 1, completed: 0)
What if the property whose name didn’t match the JSON wasn’t optional? Let’s try changing one of the non-optional property names:
struct Todo: Codable {
var displayTitle: String
var serverId: Int?
var userId: Int
var completed: Int
}
We’ve changed title
to displayTitle
.
If we don’t use CodingKeys
then we’ll get an error when we call try decoder.decode(Todo.self, from: responseData)
:
keyNotFound(grok101.Todo.(CodingKeys in _6B02657B511E8A9934EF1ACB6A92D55E).displayTitle,
Swift.DecodingError.Context(codingPath: [Optional(grok101.Todo.(CodingKeys in _6B02657B511E8A9934EF1ACB6A92D55E).displayTitle)],
debugDescription: "Key not found when expecting non-optional type String for coding key \"displayTitle\""))
Which is telling us that it can’t find the JSON key to match up with the non-optional displayTitle
property.
Let’s try using CodingKeys
to handle this issue:
enum CodingKeys: String, CodingKey {
case displayTitle = "title"
case serverId = "id"
case userId
case completed
}
Success! That works:
Todo(displayTitle: "delectus aut autem", serverId: Optional(1), userId: 1, completed: 0)
The same CodingKeys
will work with the functions to get all the Todo
items and to save a new Todo
so we don’t need to change anything else to support those API calls.
Parsing a Different Structure
Sometimes the item you want in your Swift code has a different structure than the JSON you’re getting. You might have to dig through a bit chunk of JSON to get a few properties that you want or you might have an item that’s split up between a few elements in the JSON.
So far we’ve been ignoring the result that we get when we create a new Todo
. This what it looks like:
{
"{\"title\":\"First todo\",\"userId\":1,\"completed\":0}": "",
"id": 201
}
There are 2 elements in the JSON: what we sent (oddly as a key) and the id
for the newly created Todo
. What we really want is the new id
which we could add to our Todo
. First let’s update the functions to parse & expect a Todo
.
For the calling function, we’ll add a second parameter to the completion handler:
func saveNewTodo() {
let newTodo = Todo(displayTitle: "First todo", serverId: nil, userId: 1, completed: 0)
newTodo.save { (todo, error) in
if let error = error {
// got an error in getting the data, need to handle it
print("error calling POST on /todos")
print(error)
return
}
guard let todo = todo else {
print("did not get todo in response from creating todo")
return
}
// success!
print("Saved new todo")
debugPrint(todo)
}
}
For the save()
function, we also need to add that second parameter to the completion handler:
func save(completionHandler: @escaping (Todo?, Error?) -> Void)
Update all the calls to the completion handler to use 2 parameters:
func save(completionHandler: @escaping (Todo?, Error?) -> Void) {
let todosEndpoint = Todo.endpointForTodos()
guard let todosURL = URL(string: todosEndpoint) else {
let error = BackendError.urlError(reason: "Could not construct URL")
completionHandler(nil, error)
return
}
var todosUrlRequest = URLRequest(url: todosURL)
todosUrlRequest.httpMethod = "POST"
let encoder = JSONEncoder()
do {
let newTodoAsJSON = try encoder.encode(self)
todosUrlRequest.httpBody = newTodoAsJSON
// See if it's right
if let bodyData = todosUrlRequest.httpBody {
print(String(data: bodyData, encoding: .utf8) ?? "no body data")
}
} catch {
print(error)
completionHandler(nil, error)
}
let session = URLSession.shared
let task = session.dataTask(with: todosUrlRequest, completionHandler: {
(data, response, error) in
guard error == nil else {
completionHandler(nil, error!)
return
}
// TODO: parse response
completionHandler(nil, nil)
})
task.resume()
}
And try to parse the response:
let task = session.dataTask(with: todosUrlRequest, completionHandler: {
(data, response, error) in
guard error == nil else {
let error = error!
completionHandler(nil, error)
return
}
guard let responseData = data else {
let error = BackendError.objectSerialization(reason: "No data in response")
completionHandler(nil, error)
return
}
let decoder = JSONDecoder()
do {
let todo = try decoder.decode(Todo.self, from: responseData)
completionHandler(todo, nil)
} catch {
print("error parsing response from POST on /todos")
print(error)
completionHandler(nil, error)
}
})
task.resume()
But that won’t work because the JSON isn’t in the format that it expects for a Todo
:
keyNotFound(grok101.Todo.CodingKeys.displayTitle,
Swift.DecodingError.Context(codingPath: [Optional(grok101.Todo.CodingKeys.displayTitle)],
debugDescription: "Key not found when expecting non-optional type String for coding key \"displayTitle\""))
How can we tell the decoder to parse that JSON? The simplest way is to create a new type that mirrors that JSON then create Todo
from that item.
Since we only need the id
we’ll just pull that out from the JSON to define our new struct that encapsulates the response from creating a Todo
:
struct CreateTodoResult: Codable {
var id: Int?
}
We’ll specify that we’re parsing a CreateTodoResult
then get the id
from it and add it to the Todo
before returning it:
let decoder = JSONDecoder()
do {
let createResult = try decoder.decode(CreateTodoResult.self, from: responseData)
var todo = self
todo.serverId = createResult.id
completionHandler(todo, nil)
} catch {
print("error parsing response from POST on /todos")
print(error)
completionHandler(nil, error)
}
If you can’t easily generate the structure to match the JSON and work with that, you can override the decode
function. For example, if we didn’t want the user’s address to be part of a nested struct:
struct User: Codable {
let id: Int?
let name: String
let email: String
let city: String
let zipcode: String
// ...
}
To handle the nested structure in the JSON (but not in our struct), we define the CodingKeys
including one for address
. Then we add another enumc called AddressKeys
that’s similar to CodingKeys
and has the keys that we want to parse inside of the Address JSON object.
struct User: Decodable {
// ...
enum CodingKeys: String, CodingKey {
case id
case name
case email
case address
}
enum AddressKeys: String, CodingKey {
case city
case zipcode
}
// ...
}
Now we can override the initializer that’s used by the decoder when it creates a User
from JSON. That’s where we’ll be able to write our own code to map the values from the JSON into our struct’s properties. Place this initializer in an extension to that you’ll still get the default memberwise initializer (otherwise we won’t be able to call self.init(displayTitle: title, serverId: serverId, userId: userId, completed: completed)
without writing our own version of that function):
extension User {
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let id = try values.decodeIfPresent(Int.self, forKey: .id)
let name = try values.decode(String.self, forKey: .name)
let email = try values.decode(String.self, forKey: .email)
let addressValues = try values.nestedContainer(keyedBy: AddressKeys.self, forKey: .address)
let city = try addressValues.decode(String.self, forKey: .city)
let zipcode = try addressValues.decode(String.self, forKey: .zipcode)
self.init(id: id, name: name, email: email, city: city, zipcode: zipcode)
}
}
decoder.container(keyedBy: CodingKeys.self)
will look at the JSON and try to pull out each element using the CodingKeys
. Then for each property do something like values.decodeIfPresent(Int.self, forKey: .id)
to try to extract that value. The enums for the keys are based on the CodingKeys
so you’ll have one for each property.
For the elements within the address key, we have to get the container based on that key: values.nestedContainer(keyedBy: AddressKeys.self, forKey: .address)
. Then we can extract the contents like addressValues.decode(String.self, forKey: .city)
.
Sending a Different Structure
To send a different structure we can implement a custom version of the encode
function. For example, we might want to just send one parameter to update using a PATCH request. Suppose we wanted to update the user’s email address.
Change the declaration from let
to var
so we can change it:
struct User: Codable {
let id: Int?
let name: String
var email: String
let city: String
let zipcode: String
// ...
}
And add the encode
function to an extension. Here’s how to include only the email parameter:
extension User {
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(email, forKey: .email)
}
}
Then we can take a user, update their email address locally, and send it to the server:
var editedUser = user
editedUser.email = "a@aol.com"
editedUser.update() {
error in
print(error ?? "no error")
}
And create that update()
function to send the PATCH request:
func update(completionHandler: @escaping (Error?) -> Void) {
guard let id = self.id else {
let error = BackendError.urlError(reason: "No ID provided for PATCH")
completionHandler(error)
return
}
let endpoint = User.endpointForID(id)
guard let url = URL(string: endpoint) else {
let error = BackendError.urlError(reason: "Could not construct URL")
completionHandler(error)
return
}
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "PATCH"
var headers = urlRequest.allHTTPHeaderFields ?? [:]
headers["Content-Type"] = "application/json"
urlRequest.allHTTPHeaderFields = headers
let encoder = JSONEncoder()
do {
let asJSON = try encoder.encode(self)
urlRequest.httpBody = asJSON
// See if it's right
if let bodyData = urlRequest.httpBody {
print(String(data: bodyData, encoding: .utf8) ?? "no body data")
}
} catch {
print(error)
completionHandler(error)
}
let session = URLSession.shared
let task = session.dataTask(with: urlRequest) {
(data, response, error) in
guard error == nil else {
let error = error!
completionHandler(error)
return
}
guard let responseData = data else {
let error = BackendError.objectSerialization(reason: "No data in response")
completionHandler(error)
return
}
print(String(data: responseData, encoding: .utf8) ?? "No response data as string")
completionHandler(nil)
}
task.resume()
}
It’s nearly the same as the save()
function except the HTTP method and adding a “Content-Type: application/json” header:
urlRequest.httpMethod = "PATCH"
var headers = urlRequest.allHTTPHeaderFields ?? [:] // create empty dictionary if headers is nil
headers["Content-Type"] = "application/json"
urlRequest.allHTTPHeaderFields = headers
Before sending the request we can check to make sure the body is set properly:
if let bodyData = urlRequest.httpBody {
print(String(data: bodyData, encoding: .utf8) ?? "no body data")
}
Which prints what we expect:
{"email":"a@aol.com"}
And after the network call is done, we can print the response body to check that the email address is changed:
guard let responseData = data else {
let error = BackendError.objectSerialization(reason: "No data in response")
completionHandler(error)
return
}
print(String(data: responseData, encoding: .utf8) ?? "No response data as string")
Which prints:
{
"id": 1,
"name": "Leanne Graham",
"username": "Bret",
"email": "a@aol.com",
// ...
}
And that email address has been successfully changed :)
Some Warnings
Since your JSON parsing depends on the names of the properties make sure you don’t rename them (unless the JSON changes too). This would be a good thing to write tests for so that you’ll notice if you ever break the JSON encoding/decoding by renaming a property.
If you define init(from decoder: Decoder)
or encode(to encoder: Encoder)
to do custom decoding or encoding then it’ll be used for all of your encoding & decoding of that class. E.g., that email-only encoding for the User
would be used if we created a new function to create new users, which is probably not what we want.
And That’s All for Parsing JSON in Swift 4 Today
Codable
makes it simpler and cleaner to parse JSON in Swift 4. It’s flexible enough to handle discrepancies in keys and structure in the JSON relative to our code. So we don’t need to have ugly variable names like json_key_with_underscores
in our Swift code.
For more info on this topic, Apple has some newly released docs:
Sample Code (Playground): Using JSON with Custom Types
Encoding and Decoding Custom Types
That’s all for today but in future posts we’ll be looking at Codable
again, including handling dates and encoding/decoding of JSON that isn’t all key-value pairs.
If you’d like more Swift tutorials on topics like this one, sign up below to get them sent directly to your inbox.