Decode JSON in Swift with intermediate types

Jeroen de Vrind
Mar 1 · 4 min read
Photo by Jantine Doornbos on Unsplash

To decode a JSON structure and encode the model in your app back to JSON, you can conform your model to the Swift Codable protocol. This is a combination of two other protocols: Decodable and Encodable.

This is great for simple JSON arrays and dictionaries like:

let json = """
[
{
"name": "Annual Travel Insurance",
"paymentFrequency": "yearly",
"description": "Travel safe all year round."
},
{
"name": "Car Insurance",
"paymentFrequency": "monthly"
}
]
""".data(using: .utf8)!

You can conform your model to the Codable (or Decodable) protocol:

struct BankProduct: Codable {
var name: String
var paymentFrequency: PaymentFrequency
var description: String?

enum PaymentFrequency: String, Codable {
case monthly
case yearly
}
}

And everything will be automatically decodable, as long as there is no other element in the JSON array that is not representing our model. Otherwise the decoding will fail. This means that we can add in each element of the JSON structure extra keys, but the keys name and category should always be in the element because they are not optional in our model.

In this example there is also an enum added, so you can see that it automatically converts the JSON to the right cases, as long as the enum conforms to the (De)Codable protocol and the enum’s case names are the same as the strings in the JSON. Otherwise you need to provide a rawValue for the cases.

We can create objects from the JSON with:

let decoder = JSONDecoder()
let products = try decoder.decode([BankProduct].self, from: json)

When the key names of the JSON don’t match the names of the properties in our model, we need a nested enum named CodingKeys with a String rawValue type that conforms to the CodingKey protocol. The enum should contain all the keys, even the ones that have equal names in the model and the JSON. But for these last ones you don’t need to provide a rawValue.

let json = """
[
{
"product_name": "Annual Travel Insurance",
"payment_frequency": "yearly",
"description": "Travel safe all year round."
},
{
"product_name": "Car Insurance",
"payment_frequency": "monthly"
}
]
""".data(using: .utf8)!
struct BankProduct: Codable {
...

private enum CodingKeys: String, CodingKey {
case name = "product_name"
case paymentFrequency = "payment_frequency"
case description
}

}

But what do you need to do when the structure of the concepts you are modeling in your app are inconsistent with the structure of the JSON?

It could be that some data of your model is spread out among several nested objects in the JSON. In the next example you will see that the JSON returned by an API contains more information than needed and the data we want to model is nested inside other structures:

let json = """
[
{
"name": "Bank 1",
"categories": [
{
"name": "Insurances",
"subcategories": [
{
"name": "Travel",
"product": {
"name": "Annual Travel Insurance",
"paymentFrequency": "yearly",
"description": "Travel safe all year round."
}
},
{
"name": "Car",
"product": {
"name": "Car insurance",
"paymentFrequency": "monthly"
}
}
]
}
]
},
{
"name": "Bank 2",
"categories": [
{
"name": "Payments",
"subcategories": [
{
"name": "CreditCard",
"product": {
"name": "Platinumcard",
"paymentFrequency": "yearly"
}
}
]
}
]
}
]
""".data(using: .utf8)!

In our example we have a type that represents the bank and a list of products:

struct Bank {
var name: String
var products: [Product]
struct Product: Codable {
var name: String
var paymentFrequency: PaymentFrequency
var description: String?
}
}

You see that the Bank structure has an incompatibility with the JSON structure that has the products inside the subcategories and categories structure. To bridge this gap we can create an intermediate type:

struct BankService: Decodable {
let name: String
let categories: [Category]
struct Category: Decodable {
let name: String
let subcategories: [Subcategory]
struct Subcategory: Decodable {
let name: String
let product: Bank.Product
}
}
}

The BankService matches now the structure of the JSON. And the conformance to Decodable is automatic when the protocol is included in the nested types as well. The Bank's nested Product type is reused here in the Subcategory type because the names and types inside are the same. In an extension on Bank we can now add an initializer that takes a BankService instance and removes the nesting:

extension Bank {
init(from service: BankService) {
name = service.name
products = []
for category in service.categories {
for subcategory in category.subcategories {
products.append(subcategory.product)
}
}
}
}

You can now read the JSON, pass it through the intermediate type and use the resulting Bank instances in your app:

let decoder = JSONDecoder()
let services = try decoder.decode([BankService].self, from: json)
let banks = services.map { Bank(from: $0) }

Short Swift Stories

Articles about Swift that take less than 10 minutes to read.

Jeroen de Vrind

Written by

Short Swift Stories

Articles about Swift that take less than 10 minutes to read.