Everything you need to know to start encoding and decoding JSONs in Swift
Let’s start simple in Swift and understand some of the core concepts. In Swift, we have to fundamental protocols we need to get familiar with: Encodable
and Decodable
. Encodable
allows us to convert a Swift struct into a JSON string. Decodable
does the opposite. Codable
is just a short hand for Encodable
and Decodable
.
Let’s make a simple struct and start to play with this
1struct Person: Codable {
2 let name: String
3 let age: Int
4 let email: String
5}
Since this struct is marked Codable we can encode and decode it. Doing that is very simple. Let’s look at an example of that
1import Foundation
2
3let person = Person(name: "John Doe", age: 30, email: "johndoe@example.com")
4
5// Convert to JSON
6let jsonData = try! JSONEncoder().encode(person)
7let jsonString = String(data: jsonData, encoding: .utf8)!
8
9print("JSON Output: \(jsonString)")
10
11// Conver to Person
12let personData = jsonString.data(using: .utf8)!
13let decodedPerson = try! JSONDecoder().decode(Person.self, from: personData)
14
15print("Decoded Struct: \(decodedPerson)")
Which would produce the following output
JSON Output: {"email":"johndoe@example.com","age":30,"name":"John Doe"}
Decoded Struct: Person(name: "John Doe", age: 30, email: "johndoe@example.com")
Let’s go a step further. Let’s say that we have some struct that we want to customise the encoding and decoding to.
1struct Event {
2 let name: String
3 let date: Date
4 let attendees: Int
5}
In this example, we have a Date and what we want to do is format that date in a specific way. We can implement Encodable like this.
1extension Event: Encodable {
2 func encode(to encoder: Encoder) throws {
3 var container = encoder.container(keyedBy: CodingKeys.self)
4
5 // Encode name and attendees as usual
6 try container.encode(name, forKey: .name)
7 try container.encode(attendees, forKey: .attendees)
8
9 // Encode the date as a formatted string
10 let dateFormatter = DateFormatter()
11 dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ssZ"
12 let dateString = dateFormatter.string(from: date)
13 try container.encode(dateString, forKey: .date)
14 }
15
16 enum CodingKeys: String, CodingKey {
17 case name
18 case date
19 case attendees
20 }
21}
Notice the CodingKeys
enum you need to make - these will be the names of the keys in the JSON. You can customise these as much as you like.
We can implement Deocdable
in much the same way
1extension Event: Decodable {
2 init(from decoder: Decoder) throws {
3 let container = try decoder.container(keyedBy: CodingKeys.self)
4
5 // Decode name and attendees as usual
6 name = try container.decode(String.self, forKey: .name)
7 attendees = try container.decode(Int.self, forKey: .attendees)
8
9 // Decode the date from a formatted string
10 let dateString = try container.decode(String.self, forKey: .date)
11 let dateFormatter = DateFormatter()
12 dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ssZ"
13
14 guard let date = dateFormatter.date(from: dateString) else {
15 throw DecodingError.dataCorruptedError(
16 forKey: .date, in: container, debugDescription: "Invalid date format")
17 }
18 self.date = date
19 }
20}
Again, let’s make a little demonstration
1let event = Event(name: "Swift Conference", date: Date(), attendees: 200)
2
3let jsonData = try! JSONEncoder().encode(event)
4let jsonString = String(data: jsonData, encoding: .utf8)!
5
6print("JSON Output: \(jsonString)")
7
8// Conver to Person
9let personData = jsonString.data(using: .utf8)!
10let decodedPerson = try! JSONDecoder().decode(Event.self, from: personData)
11
12print("Decoded Struct: \(decodedPerson)")
Which produces the following output
JSON Output: {"name":"Swift Conference","attendees":200,"date":"2024-09-27 12:59:34+0100"}
Decoded Struct: Event(name: "Swift Conference", date: 2024-09-27 11:59:34 +0000, attendees: 200)
In Swift, generally we use camelCase for struct keys. But, many people will use snake_case for JSON keys. You might be sending a JSON to a HTTP API, or receiving it. You can always just make the keys in the struct snake_case, but there is another way. Let’s say we have this struct
1 struct User: Codable {
2 let firstName: String
3 let secondName: String
4 let age: Int
5}
We can actually very easily convert this to a snake_case JSON with the following.
1let user = User(firstName: "Bob", secondName: "Smith", age: 22)
2
3let encoder = JSONEncoder()
4encoder.outputFormatting = [.prettyPrinted]
5encoder.keyEncodingStrategy = .convertToSnakeCase
6
7let jsonData = try! encoder.encode(user)
8let jsonString = String(data: jsonData, encoding: .utf8)!
9
10print(jsonString)
Notice how we just make a JSONEncoder and then modify some of its properties. We also set it to pretty print, so that the output will be the following
1{
2 "age" : 22,
3 "first_name" : "Bob",
4 "second_name" : "Smith"
5}
There’s other formatting options too, like .sortedKeys
which sorts the keys in alphabetical order.
Decoding is very much the same, but in reverse (continuing on from the last example)
1let decoder = JSONDecoder()
2decoder.keyDecodingStrategy = .convertFromSnakeCase
3
4let userData = jsonString.data(using: .utf8)!
5let decodedUser = try! decoder.decode(User.self, from: userData)
6
7print(decodedUser)
Which produces the output we expect
User(firstName: "Bob", secondName: "Smith", age: 22)
It’s important to consider error handling. Since JSONs are literally just text, your program could encounter anything. It’s important to consider what might happen if the JSON fails to encode or decode - in these examples we’ve just been unwrapping everything but that isn’t recommended in production.
One way to handle errors is to use optional fields. For example, if we made the struct
1 struct User: Codable {
2 let firstName: String
3 let secondName: String
4 let age: Int?
5}
And the program encountered
{
"first_name" : "Bob",
"second_name" : "Smith"
}
Then we would safely convert it to the object
1User(firstName: "Bob", secondName: "Smith", age: nil)
Alternatively we could use the custom decoder to create default values
1struct User: Codable {
2 let name: String
3 let email: String
4 let age: Int
5
6 init(from decoder: Decoder) throws {
7 let container = try decoder.container(keyedBy: CodingKeys.self)
8 name = try container.decode(String.self, forKey: .name)
9 email = try container.decodeIfPresent(String.self, forKey: .email) ?? "unknown@example.com"
10 age = try container.decodeIfPresent(Int.self, forKey: .age) ?? 0
11 }
12}
That’s pretty much everything you need to get started with JSONs in Swift. As you can see, the language is really powerful and comes with a lot of built in support for JSONs!