Coding Keys in Swift with Decodable: How and When to Use Them

You can easily parse JSON data in Swift by creating a Decodable type that matches the structure of a JSON object.

However, we often need to decode JSON data that does not match the model types of our apps. In those cases, we need to control the decoding using coding keys.



FREE GUIDE: Parsing JSON in Swift - The Ultimate Cheat Sheet

Learn the fundamentals of JSON parsing in Swift, advanced decoding techniques, and tasks for app development

DOWNLOAD THE FREE GUIDE

Table of contents

How CodingKeys work and when you need them

At its simplest, decoding JSON data in Swift does not require coding keys. All you need is a Decodable Swift type that matches the data you want to decode.

let json = """
{
	"name": "Rembrandt Harmenszoon van Rijn",
	"birthplace": "Leiden, Dutch Republic",
	"profession": "Painter"
}
""".data(using: .utf8)!

struct Artist: Decodable {
	let name: String
	let birthplace: String
	let profession: String
}

let artist = try JSONDecoder().decode(Artist.self, from: json)

However, that is often not practical.

Swift types should be modeled after your app’s business logic and not merely follow the data formats received by your app.

That means you often need to control the decoding of JSON data to add logic fine-tuned to your app’s data representation.

Renaming the fields of a JSON object to follow Swift’s conventions

The most common use for coding keys is to rename the fields of JSON data into stored properties that follow the Swift API Design Guidelines.

Since JSON data usually comes from an Internet server, it often follows the naming conventions of web development.

One of these conventions is using snake case instead of camel case, which is Swift’s naming convention.

If you only need to change the naming convention, you do not need to use coding keys. Setting the keyDecodingStrategy property of the JSONDecoder to .convertFromSnakeCase is enough.

let json = """
{
	"first_name": "Rembrandt",
	"last_name": "van Rijn",
	"profession": "Painter"
}
""".data(using: .utf8)!

struct Artist: Decodable {
	let firstName: String
	let lastName: String
	let profession: String
}

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let artist = try decoder.decode(Artist.self, from: json)

Coding keys are required to completely rename some or all of a JSON object’s fields.

You must name your coding keys following your type’s property names while using a String raw value for the corresponding field in the JSON data.

let json = """
{
	"first_name": "Rembrandt",
	"last_name": "van Rijn",
	"profession": "Painter"
}
""".data(using: .utf8)!

struct Artist: Decodable {
	let name: String
	let surname: String
	let profession: String

	enum CodingKeys: String, CodingKey {
		case name = "first_name"
		case surname = "last_name"
		case profession
	}
}

let artist = try JSONDecoder().decode(Artist.self, from: json)

Selecting the stored properties of a decodable Swift type that must be decoded

Another everyday use for coding keys is to select which properties must be encoded or decoded. Your Swift types often have extra properties used by your app’s logic that are not present in the JSON data.

The JSONDecoder class tries to decode all the properties of a Decodable type. If any of those properties are absent in the JSON data, it will throw an error.

Using coding keys, you can indicate the specific properties to be decoded.

let json = """
{
	"name": "Rembrandt Harmenszoon van Rijn",
	"profession": "Painter"
}
""".data(using: .utf8)!

struct Artist: Decodable {
	let name: String
	let profession: String
	var viewCount: Int = 0
	var isFavorite: Bool = false

	enum CodingKeys: CodingKey {
		case name
		case profession
	}
}

let artist = try JSONDecoder().decode(Artist.self, from: json)

Adding extra logic to the decoding process through a decoding initializer

When the properties of your Swift types don’t match the JSON data your app receives from outside resources, decoding data is not straightforward and requires extra logic.

In those cases, you must provide coding keys matching the fields in the JSON data. Moreover, you also must add a decoding initializer where you perform additional computations.

For example, our app might represent an artist’s name using a single name property, while the JSON data we need to decode might separate the first, middle, and last names. We combine these three fields in the decoding initializer.

let json = """
{
	"first_name": "Rembrandt",
	"middle_name": "Harmenszoon",
	"last_name": "van Rijn",
}
""".data(using: .utf8)!

struct Artist: Decodable {
	let name: String
	let profession: String?

	enum CodingKeys: String, CodingKey {
		case firstName
		case middleName
		case lastName
		case profession
	}

	init(from decoder: any Decoder) throws {
		let container = try decoder.container(keyedBy: CodingKeys.self)
		let firstName = try container.decode(String.self, forKey: .firstName)
		let middleName = try container.decode(String.self, forKey: .middleName)
		let lastName = try container.decode(String.self, forKey: .lastName)
		self.name = firstName + " " + middleName + " " + lastName
		self.profession = try container.decodeIfPresent(String.self, forKey: .profession)
	}
}

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let artist = try decoder.decode(Artist.self, from: json)

Remember that when you provide a decoding initializer, you must explicitly decode all properties. Swift initializers require you to assign a value to all stored properties, so it is not possible to decode only some properties while relying on automatic decoding for others.

Beware that the decode(_:forKey:) method of KeyedDecodingContainer throws an error when a field is missing from the JSON data. In that case, make the corresponding property optional and decode its value using the decodeIfPresent(_:forKey:) method instead.

Flattening nested JSON object into a single Swift model type

One last use for coding keys is to flatten nested JSON data. Often, a remote API has a more complex representation of its data than our app’s logic requires.

While it’s possible to decode nested JSON data by creating corresponding nested Swift types, these extra types are often unneeded in your apps.

For example, the JSON data we want to decode might contain an artist’s birthplace and spouse name inside nested JSON objects.

let json = """
{
	"name": "Rembrandt Harmenszoon van Rijn",
	"born": {
		"location": "Leiden, Dutch Republic",
		"date": "1606-07-15"
	},
	"profession": "Painter",
	"spouse": {
		"name": "Saskia van Uylenburgh",
		"born": {
			"location": "Leeuwarden, Friesland",
			"date": "1612-08-02"
		}
	}
}
""".data(using: .utf8)!

However, our app might not need those extra nested types, so it’s more practical not to nest that information inside another type.

To decode a nested JSON object, we can create coding keys for its properties and use them to get a nested container using the nestedContainer(keyedBy:forKey:) method of KeyedDecodingContainer. Then, we use that container to decode the properties we want to flatten into our type.

struct Artist: Decodable {
	let name: String
	let birthplace: String
	let profession: String
	let spouse: String

	enum CodingKeys: CodingKey {
		case name
		case born
		case profession
		case spouse
	}

	enum BornKeys: CodingKey {
		case location
	}

	enum SpouseKeys: CodingKey {
		case name
	}

	init(from decoder: any Decoder) throws {
		let container = try decoder.container(keyedBy: CodingKeys.self)
		self.name = try container.decode(String.self, forKey: .name)
		self.profession = try container.decode(String.self, forKey: .profession)

		let bornContainer = try container.nestedContainer(keyedBy: BornKeys.self, forKey: .born)
		self.birthplace = try bornContainer.decode(String.self, forKey: .location)

		let spouseContaineer = try container.nestedContainer(keyedBy: SpouseKeys.self, forKey: .spouse)
		self.spouse = try spouseContaineer.decode(String.self, forKey: .name)
	}
}

let artist = try JSONDecoder().decode(Artist.self, from: json)

Coding keys can also be used to decode JSON data with dynamic keys. However, in that case, Swift enums do not work. You have to construct a dynamic coding key structure instead.

If you want a quick reference of how and when to use coding keys, together with other JSON parsing techniques, download my free cheat sheet below.

FREE GUIDE: Parsing JSON in Swift - The Ultimate Cheat Sheet

Learn the fundamentals of JSON parsing in Swift, advanced decoding techniques, and tasks for app development

DOWNLOAD THE FREE GUIDE

Leave a Comment