Modelling with types · core

Equatable, Hashable, Comparable and Codable

Four protocols the compiler will implement for you, and the rules that decide when it can.

10 min read12 min practice0/2 exercises4 recall cards

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
Guess firstAnswering before you read makes the explanation stick — even when you get it wrong.

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
    }
}
Swift

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 ordercase low, medium, high sorts exactly as written. Everywhere else you implement < and get >, <=, >=, sorted(), min() and max() for free.

Swift

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 = .iso8601
Predict, then runA wrong prediction you have committed to is worth more than a right answer you read.

Hashable 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)
Check yourself

You write a custom == for a struct that compares only id, but leave Hashable synthesised. What breaks?

Make it sortable

Write the code

Make Task2 sortable by priority (high first), then by title alphabetically, using a single < implementation.

Solution unlocks after 3 attempts

Equal by identity

Write the code

Make two Account2 values compare equal when their number matches, even if the balance differs — and keep hashing consistent.

Solution unlocks after 3 attempts
Explain itSaved on this device. Never graded.

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.

Checkpoint

You can now:

  • Say when synthesis applies and when you must implement a conformance by hand
  • Keep == and hash(into:) consistent
  • Implement Comparable with one operator and the tuple trick
  • Decode JSON with CodingKeys or a key strategy

Next up: error handling — Swift's throws, and why it is neither exceptions nor error codes.

How well do you know this now? Rating yourself honestly, then being tested on it, is how you find out where your intuition is wrong.