A Gentle Introduction to JSON in Swift

Author - 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

struct Person: Codable {
  let name: String
  let age: Int
  let email: String
}

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

import Foundation

let person = Person(name: "John Doe", age: 30, email: "johndoe@example.com")

// Convert to JSON
let jsonData = try! JSONEncoder().encode(person)
let jsonString = String(data: jsonData, encoding: .utf8)!

print("JSON Output: \(jsonString)")

// Conver to Person
let personData = jsonString.data(using: .utf8)!
let decodedPerson = try! JSONDecoder().decode(Person.self, from: personData)

print("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.

struct Event {
  let name: String
  let date: Date
  let attendees: Int
}

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.

extension Event: Encodable {
  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)

    // Encode name and attendees as usual
    try container.encode(name, forKey: .name)
    try container.encode(attendees, forKey: .attendees)

    // Encode the date as a formatted string
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ssZ"
    let dateString = dateFormatter.string(from: date)
    try container.encode(dateString, forKey: .date)
  }

  enum CodingKeys: String, CodingKey {
    case name
    case date
    case attendees
  }
}

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

extension Event: Decodable {
  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)

    // Decode name and attendees as usual
    name = try container.decode(String.self, forKey: .name)
    attendees = try container.decode(Int.self, forKey: .attendees)

    // Decode the date from a formatted string
    let dateString = try container.decode(String.self, forKey: .date)
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ssZ"

    guard let date = dateFormatter.date(from: dateString) else {
      throw DecodingError.dataCorruptedError(
        forKey: .date, in: container, debugDescription: "Invalid date format")
    }
    self.date = date
  }
}

Again, let’s make a little demonstration

let event = Event(name: "Swift Conference", date: Date(), attendees: 200)

let jsonData = try! JSONEncoder().encode(event)
let jsonString = String(data: jsonData, encoding: .utf8)!

print("JSON Output: \(jsonString)")

// Conver to Person
let personData = jsonString.data(using: .utf8)!
let decodedPerson = try! JSONDecoder().decode(Event.self, from: personData)

print("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

 struct User: Codable {
  let firstName: String
  let secondName: String
  let age: Int
}

We can actually very easily convert this to a snake_case JSON with the following.

let user = User(firstName: "Bob", secondName: "Smith", age: 22)

let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted]
encoder.keyEncodingStrategy = .convertToSnakeCase

let jsonData = try! encoder.encode(user)
let jsonString = String(data: jsonData, encoding: .utf8)!

print(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

{
  "age" : 22,
  "first_name" : "Bob",
  "second_name" : "Smith"
}

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)

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

let userData = jsonString.data(using: .utf8)!
let decodedUser = try! decoder.decode(User.self, from: userData)

print(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

 struct User: Codable {
  let firstName: String
  let secondName: String
  let age: Int?
}

And the program encountered

{
  "first_name" : "Bob",
  "second_name" : "Smith"
}

Then we would safely convert it to the object

User(firstName: "Bob", secondName: "Smith", age: nil)

Alternatively we could use the custom decoder to create default values

struct User: Codable {
    let name: String
    let email: String
    let age: Int

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        name = try container.decode(String.self, forKey: .name)
        email = try container.decodeIfPresent(String.self, forKey: .email) ?? "unknown@example.com"
        age = try container.decodeIfPresent(Int.self, forKey: .age) ?? 0
    }
}

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.