A Gentle Introduction to JSON in Swift

Everything you need to know to start encoding and decoding JSONs in Swift

Indigo Curnick
September 30, 2024
Articles

Simple JSONs

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")

Customising the Encoding and Decoding

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)

Key Encoding Strategies

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)

Optional Fields

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!

Subscribe To Our Newsletter - Sleek X Webflow Template

Subscribe to our newsletter

Sign up at Naurt for product updates, and stay in the loop!

Thanks for subscribing to our newsletter
Oops! Something went wrong while submitting the form.