This thing I'm doing seems to work, but I'm not sure if I can rely on it from Codable
semantics or if it's a happy accident of JSONEncoder/Decoder (and perhaps the iOS/Xcode versions I'm using).
I have a bunch of types that all share some standard boilerplate properties, call that the "standard metadata". I used @dynamicMemberLookup
to compose the standard metadata with structs for the unique properties of various types, like this:
public struct StandardMetadata: Codable {
public let created: Date
public let lastChanged: Date
public let id: UInt
}
@dynamicMemberLookup
public struct ComesFromAPI<Model> {
public let metadata: StandardMetadata
let body: Model
public subscript<Value>(dynamicMember keyPath: KeyPath<Model, Value>) -> Value {
body[keyPath: keyPath]
}
}
public struct SomeExampleModel: Codable {
let name: String
}
I'm receiving these types as JSON, and the metadata is in the same container as the individual values.
{ "created": "2022-01-01T00:00:00",
"lastChanged": "2022-03-10T02:05:23",
"id": 3527,
"name": "Gollum"
}
I can give ComesFromAPI
a conditional conformance like so:
extension ComesFromAPI: Codable where Model: Codable {
public init(from decoder: Decoder) throws {
metadata = try StandardMetadata(from: decoder)
body = try Model(from: decoder)
}
public func encode(to encoder: Encoder) throws {
try metadata.encode(to: encoder)
try body.encode(to: encoder)
}
}
This lets me successfully round-trip encode/decode ComesFromAPI<SomeExampleModel>
, with the encoded JSON looking as I want it to (all properties in one JSON container). (That is, it's successful compiling with Xcode Version 13.3.1 and targeting iOS 15.4.)
But I don't understand the semantic guarantees of Codable
well enough to know if this is reliable; I would not have been surprised at all if the encoding instead generated two JSON objects side-by-side, and while I'm delighted that it doesn't I don't really understand why. Can anyone enlighten me?