Swift 4's key paths provide a great way for referencing a type's properties in a Swifty way. Since Swift 4's release, many ORMs have adopted key paths in lieu of strings for doing query building. Below are a couple examples from real ORMs built on Swift 4.
Fluent (SQL / NoSQL)
Planet.query(on: conn).filter(\.type == .smallRocky).all()
Perfect CRUD (SQL)
personTable.order(by: \.lastName, \.firstName)
Kuery (Core Data)
Query(Person.self).filter(\Person.age > 20)
Advantages
KeyPaths offer a couple key advantages over Strings:
- Type-safe: The compiler helps guarantee you've spelled the key path correctly.
- Auto-completable: If the force is strong with you, Xcode's autocomplete can help you fill in key paths automatically.
- Strong type info: Key paths know the type of their value allowing you to use leading dot syntax.
Comparing Fluent version 2 (pre Swift 4) to 3 helps highlight this:
// Fluent 2
Planet.makeQuery().filter("type" == PlanetType.smallRocky).all()
// Fluent 3
Planet.query(on: conn).filter(\.type == .smallRocky)
The Problem
The problem with using key paths in this manner is that the relation between key paths and codable is not entirely clear. Let's take the Fluent query for example:
Planet.query(on: conn).filter(\.type == .smallRocky)
When used with a SQL database, this query should result in something along the lines of:
SELECT * FROM "planets" WHERE "type" = ? ["smallRocky"]
While this seems pretty straight forward, the conversion from \.type
to "type"
is actually quite complex.
Take the Planet
model for example:
struct Planet: Codable {
var name: String
var type: PlanetType
}
Here it's pretty clear that \.type
should yield the string "type"
. But what happens if we were to define a custom CodingKeys
enum for our type?
struct Planet: Codable {
enum CodingKeys: String, CodingKey {
case name = "name"
case type = "planet_type"
}
var name: String
var type: PlanetType
}
Given this Planet
struct, the aforementioned SQL query should now be:
SELECT * FROM "planets" WHERE "planet_type" = ? ["smallRocky"]
This is an important distinction because it means that: we need a way to relate a key path into a given Codable
type to its coding keys.
Potential API
Ideally, we could use a key path to query a codable model for the associated coding key. Spelling aside, the API could look something like this.
struct Planet: Codable {
enum CodingKeys: String, CodingKey {
case name = "name"
case type = "planet_type"
}
var name: String
var type: PlanetType
}
let key: CodingKey? = Planet.codingKey(for: \.type)
print(key?.stringValue) // Optional("planet_type")
Current Solution
The Swift ORMs I mentioned earlier have some unique (some might call hacky) ways for handling this. Fluent and Perfect CRUD use specialized decoders + some fancy tricks to determine which [CodingKey]
path is related to a given KeyPath
. Learn more here.
The CoreData based Kuery ORM is able to use the Objective-C bridged _kvcKeyPathString
value to get the string value for a key path. As far as I know, this is not affected by codable.
This issue has been touched on a bit before (see SR-5220) but I have yet to see anyone discuss key paths in relation to codable specifically.
Discussion
I would love to get the Core Team's opinion on whether this usage of key paths is something that Swift might support more formally in the future or if it is somewhat of an unintended side effect.