Codable provides a way to convert to and from formats such as JSON. In this tutorial we’ll just focus on converting JSON. The JSON format is popular among web services and provides a standard about how data can be represented. JSON formatted data can be understood by various systems making it possible to easily transfer data from one system to another regardless of operating system or programming language used.
On the iOS side, Codable is one way to convert an object or structure in to the JSON format so that the other end of the network can accept it and understand it as needed.
The Gmail API is one example of a service that provides data in JSON form. When you query the API the results come back as JSON. We don’t particularly care what programming language and operating systems are involved on the other side. All we need to know is that the response provided conforms to the JSON standard and thus, we can use Codable to convert that data in to whatever we need within our code.
Creating a Structure
Lets begin by creating a structure.
struct Message: Codable {
var id: Int
var fromName: String
var dateTime: String
var subject: String
var snippet: String
}
Here we have a Message structure that conforms to the Codable protocol. It doesn’t look any different than if we specified Codable or not, but including it does a few things in the background. The documentation tells us the structure…
...now supports the Codable methods init(from:) and encode(to:)
On basic structures, the standard implementation of init and encode are sufficient, so we won’t add anymore or override those methods in this tutorial.
Lets create some JSON data that we can use:
[{
"from_name": "DevFright.com",
"snippet": "Welcome to this weeks newsletter. We are excited...",
"subject": "Weekly Newsletter",
"date_time": "3 May 2019 3:45am",
"id": 1001
}, {
"from_name": "John Smith",
"snippet": "I received your email regarding the trip to New York...",
"subject": "Re: Flight to New York",
"date_time": "9 May 2019 8:43pm",
"id": 1002
}, {
"from_name": "Mike Fletcher",
"snippet": "What time would be good for us to meet up to discuss...",
"subject": "Re: Development Plan",
"date_time": "14 May 2019 5:29pm",
"id": 1003
}, {
"from_name": "Herbert Hillary",
"snippet": "Can you meet for lunch today at 3pm?...",
"subject": "Lunch",
"date_time": "15 May 2019 11:43am",
"id": 1004
}]
The above JSON shows that we have an array with 4 objects in it. Each object is a dictionary and has five key/values.
To decode this data we first need to get the JSON data. As we will be saving the above JSON in a file called messages.json, we need to use the following code to get the file and then convert it to Data.
func decodeMessages() -> [Message]? {
var data: Data
let file = Bundle.main.url(forResource: "messages.json", withExtension: nil)
do {
data = try Data(contentsOf: file!)
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
let decodedData = try decoder.decode([Message].self, from: data)
return decodedData
} catch {
print("Failed to decode JSON")
return nil
}
} catch {
print("Problem")
return nil
}
}
Lets look at each line of code to see what’s going on.
First, we create the method and declare that it will return an array of Messages. It is marked as optional just in case there are no messages created.
Next, we create a variable to store our Data. We use this to store the data converted from the JSON we received.
Next, we get the path to the messages.json file that we created. Typically we would fetch the JSON from a URL, but in this example it’s easier to work with a file. The principles are the same though. We need to get the JSON from somewhere. In this case, we are getting the path to where it is stored on the device.
We then use a do-try-catch to convert the contents of the file to Data.
We then initialise the JSON decoder and just after that, we specify that our decoder will convert from snake_case to camelCase. This is important. JSON typically uses snake-based names where underscores separate words. Words are also kept lower case. Objects and structs in Swift use camelCase. To keep in line with good coding practice, it is a good idea to convert from snake_case to camelCase.
When ready, we try decode the data. If the struct layout matches the JSON layout our Message struct will be populated with all the information needed. With us working with an array, it decodes each object in the array in to a Message and then puts those messages in an array.
We then return this array. If there are any issues it is typically because of not matching up the structure correctly with the JSON format.
Testing JSON Decode
Put that function in a file and class called MessageManager.swift. Assuming you have saved messages.json in your project and pasted in the JSON sample at the top of this tutorial, you can then make a call to MessageManager to fetch and decode the JSON data and return it as an array of Messages.
To do this, lets work with the project from a previous tutorial. For a more in-depth look at the views, take a look here and here.
The ContentView struct in ContentView.swift should be as follows:
struct ContentView: View {
let messageManager = MessageManager()
var body: some View {
let messages = messageManager.decodeMessages()
return List(messages!) { message in
MessageOverviewView(message: message)
}
}
}
This gets a MessageManager (which you need to create if you haven’t already, and paste in the method above called decodeMessages()), and then we fetch the messages and store the messages in a constant called messages. We then iterate over the messages and pass each message in to the MessageOverviewView.
MessageOverviewView.swift should be as follows:
struct MessageOverviewView: View {
var message: Message
var body: some View {
VStack(alignment: .leading) {
HStack {
Text(message.fromName)
.font(.title)
Spacer()
Text(message.dateTime)
.font(.footnote)
}
Text(message.subject)
.font(.headline)
Text(message.snippet)
.font(.body)
}
.padding(10)
}
}
struct MessageOverviewView_Previews: PreviewProvider {
static var previews: some View {
let message = Message(id: 1001,
fromName: "DevFright.com",
dateTime: "3 Mar 2019 4:18am",
subject: "Weekly Newsletter",
snippet: "Welcome to this weeks newsletter...")
return MessageOverviewView(message: message)
}
}
The previews at the bottom hard-code a message and then returns the view with the message passed in.
The actual view used requires we pass in a message, and then we have some stacks that organise the data in to cells; much like how an email looks. We then just fetch each attribute as needed such as the fromName, subject, etc…
One thing you will need to do is conform Message to the Identifiable protocol. Message should now look like this:
struct Message: Codable, Identifiable {
var id: Int
var fromName: String
var dateTime: String
var subject: String
var snippet: String
}
The Identifiable protocol mandates that we need a unique ID of which the JSON provides, so we can simply use what we have from the JSON to store in id.
If you have the messages.jason in place, the ContentView, MessageOverviewView, and MessageManager in place, you should now be able to run the app and you should see the four “emails” in a table view.
Testing JSON Encode
You have successfully decoded a fairly flat amount of JSON data. We will look at more advanced decodes in a future tutorial. Next, we’ll look at how to encode data.
Rather than over-complicating things by enhancing our fake email app, we’ll just look at simply encoding some manually created Message data.
Add the following to MessageManager.swift:
func encodeMessage(message: Message) {
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
let encodedData = try! encoder.encode(message)
print(String(data: encodedData, encoding: .utf8)!)
}
This is quite simple. It accepts a Message.
We then create a JSONEncoder object and then tell it to convert to snake_case.
We then try encode the data. I put the exclamation mark after try so we don’t have to over-complicate things with the do-try-catch setup.
We then print that encoded data as a string.
In ContentView, add the following just before the return List line:
let message = Message(id: 1004,
fromName: "Testing Name",
dateTime: "22 Aug 2019 10:14am",
subject: "Manual Test",
snippet: "Just testing if this works...")
messageManager.encodeMessage(message: message)
We manually create a message and then pass that message to the encoder to do its work.
The result is:
{
"date_time": "22 Aug 2019 10:14am",
"id": 1004,
"subject": "Manual Test",
"from_name": "Testing Name",
"snippet": "Just testing if this works..."
}
This matches our original JSON. The order of keys is irrelevant. When I say it matches, there is a different in that this is a single dictionary whereas our original JSON was an array of 4 dictionary items. We can modify our encoder to accept an array of messages, and then pass in an array of messages.
func encodeMessages(messages: [Message]) {
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
let encodedData = try! encoder.encode(messages)
print(String(data: encodedData, encoding: .utf8)!)
}
Modify encodeMessage and change the argument to accept an array of Messages. Also, when we encode the data we need to change from message to messages.
In content view, create a few more messages and put them in an array to pass to our encodedMessages method:
let message1 = Message(id: 1004,
fromName: "Testing Name",
dateTime: "22 Aug 2019 10:14am",
subject: "Manual Test",
snippet: "Just testing if this works...")
let message2 = Message(id: 1005,
fromName: "Sample Name",
dateTime: "25 Aug 2019 10:14am",
subject: "Second Test",
snippet: "Just testing again...")
let message3 = Message(id: 1006,
fromName: "Third Name",
dateTime: "26 Dec 2018 10:14am",
subject: "A third message",
snippet: "Does this work...")
let message4 = Message(id: 1007,
fromName: "Final Name",
dateTime: "22 Mar 2019 10:14am",
subject: "Random Test",
snippet: "Another item...")
let messagesToEncode = [message1, message2, message3, message4]
messageManager.encodeMessages(messages: messagesToEncode)
We created four messages in the sample above. We then put these in an array and pass the array to our modified message. When running the app you will now see an output of 4 messages in an array that has the same format as the JSON that we decoded. If you want to make the JSON string output look prettier to understand then paste it in to a site such as jsonlint.com and click validate.
When to Use Codable
When you want to encode and decode, use Codable. But if you only want to go one direction then use the specific Encodable or Decodable protocols.
Any questions, please ask below. In the next tutorial we’ll dig a bit deeper and look at how more complex JSON formats can be used, such as when an object has an array nested with objects nested with in that array.
Leave a Reply
You must be logged in to post a comment.