Equatable, Hashable, Comparable and Codable
Four protocols the compiler will implement for you, and the rules that decide when it can.
By the end you will be able to
- Say when Swift can synthesise a conformance and when you must write it
- Implement Comparable correctly with a single operator
- Encode and decode JSON with Codable and custom coding keys
struct Point: Equatable { var x: Int; var y: Int } — you wrote no ==. Does Point(x: 1, y: 2) == Point(x: 1, y: 2) compile and return true?
The synthesis rule
For Equatable, Hashable and Codable, Swift writes the implementation for you when:
- the type is a struct whose every stored property conforms, or
- an enum whose every associated value conforms (enums with no associated values always qualify), and
- you declare the conformance.
One freebie on top: an enum whose cases carry no associated values (raw values are fine) conforms to Equatable and Hashable automatically, without any declaration at all.
Classes never get synthesised Equatable or Hashable, because equality for a reference type is a decision only you can make: same object, or same contents? Codable is different — a class whose stored properties are all Codable does get synthesised init(from:) and encode(to:); only subclasses must handle super by hand.
Equatable
struct Point: Equatable {
var x: Int
var y: Int
}
You now get ==, !=, and everything that builds on them — contains, firstIndex(of:), Array equality.
Write your own when the default is wrong:
struct User: Equatable {
let id: UUID
var name: String
var lastSeen: Date
// Two users are the same user if the ids match, whatever else changed.
static func == (lhs: User, rhs: User) -> Bool {
lhs.id == rhs.id
}
}Hashable
Hashable requires Equatable and adds the ability to be a dictionary key or a set element. Same synthesis rule.
struct Coordinate: Hashable {
var row: Int
var column: Int
}
var visited: Set<Coordinate> = []
var scores: [Coordinate: Int] = [:]Comparable
Comparable is not synthesised for structs or classes — ordering there is a judgement, and the compiler will not guess whether a Person sorts by name, age or id. Enums are the exception: since Swift 5.3, an enum with no raw values (whose associated values, if any, are all Comparable) gets < for free, ordered by declaration order — case low, medium, high sorts exactly as written. Everywhere else you implement < and get >, <=, >=, sorted(), min() and max() for free.
The tuple trick is idiomatic and less error-prone than nested ifs:
static func < (lhs: Version, rhs: Version) -> Bool {
(lhs.major, lhs.minor) < (rhs.major, rhs.minor)
}
Tuples compare lexicographically, so this compares major first and only falls through to minor on a tie.
Codable
Codable is Encodable & Decodable, and it is how you get JSON in and out.
struct Article: Codable {
let id: Int
let title: String
let publishedAt: Date
}
let data = try JSONEncoder().encode(article)
let decoded = try JSONDecoder().decode(Article.self, from: data)
When the JSON uses different names, CodingKeys maps them:
struct Article: Codable {
let id: Int
let title: String
let publishedAt: Date
enum CodingKeys: String, CodingKey {
case id
case title = "headline"
case publishedAt = "published_at"
}
}
For the common snake_case case, you do not need CodingKeys at all:
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .iso8601Hashable synthesis uses every stored property. What does this print?
struct Tag: Hashable {
var name: String
var colour: String
}
var tags: Set<Tag> = []
tags.insert(Tag(name: "swift", colour: "orange"))
tags.insert(Tag(name: "swift", colour: "orange"))
tags.insert(Tag(name: "swift", colour: "blue"))
print(tags.count)You write a custom == for a struct that compares only id, but leave Hashable synthesised. What breaks?
Make it sortable
Make Task2 sortable by priority (high first), then by title alphabetically, using a single < implementation.
Equal by identity
Make two Account2 values compare equal when their number matches, even if the balance differs — and keep hashing consistent.
Hashable requires that equal values hash equally, but not that unequal values hash differently. Explain why the second requirement would be impossible — and what would go wrong in a Dictionary if the first were violated.
You can now:
- Say when synthesis applies and when you must implement a conformance by hand
- Keep
==andhash(into:)consistent - Implement
Comparablewith one operator and the tuple trick - Decode JSON with
CodingKeysor a key strategy
Next up: error handling — Swift's throws, and why it is neither exceptions nor error codes.