The Encoding and Decoding Custom Types documentation is a great starting point for understanding how to implement the Encodable
and Decodable
method requirements to add to your types — and specifically, the Encoding and Decoding Manually section shows how to override the compiler-synthesized implementation of these methods to provide your own.
It's important to understand how the automated approach to Codable
works, though. In a nutshell: for a type that adopts Encodable
/Decodable
, the compiler checks whether it's provided its own implementation of the associated method, and if not, attempts to fill it in on its own. There's no magic there: it fills in its internal state (manipulating the AST) with an implementation as if you had written it yourself in code.
For your example SomeAPI
type, the generated code inside of the compiler looks exactly as if you had typed the following in yourself:
struct SomeAPI: Codable {
let a: String
let b: String
let c: String
let d: String
private enum CodingKeys: String, CodingKey {
case a, b, c, d
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
a = try container.decode(String.self, forKey: .a)
b = try container.decode(String.self, forKey: .b)
c = try container.decode(String.self, forKey: .c)
d = try container.decode(String.self, forKey: .d)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(a, forKey: .a)
try container.encode(b, forKey: .b)
try container.encode(c, forKey: .c)
try container.encode(d, forKey: .d)
}
}
This only happens if you haven't implemented the protocol requirement yourself, though: if you have, the compiler leaves the existing implementation alone, assuming you've done exactly what you want.
This last point is relevant to your use-case, because it sounds like what you'd like to do is implement this type like
struct SomeAPI: Codable {
let a: String
let b: String
let c: String
let d: String
init(from decoder: Decoder) throws {
somehowCallTheCompilerSynthesizedImplementationOfThisMethod()
/* perform some other stuff */
}
}
The issue is that when you provide your own init(from:)
implementation, the compiler won't synthesize anything for your type, so there's no "default" implementation to call; unfortunately, it's an all-or-nothing deal.
To do this, then, you can either:
Implement init(from:)
on your types by implementing the full method, then adding whatever code you'd like, or
Inherit the compiler-synthesized implementation in a bit of a roundabout way: if you turn SomeAPI
into a class
, you can allow it to receive the default synthesized implementation, then override the synthesized implementation to do what you want (demonstrated here by moving its contents into a "dummy" parent type, then keep using SomeAPI
as a subclass):
class _SomeAPI: Codable {
let a: String
let b: String
let c: String
let d: String
}
class SomeAPI: _SomeAPI {
required init(from decoder: Decoder) throws {
try super.init(from: decoder)
/* do whatever */
}
}
The downside of this approach, of course, is that this changes the semantics of the SomeAPI
type, which may very well not be what you want
Approach (1) is likely a good starting approach as it keeps the semantics of this type the same, and there are tools out there that can help automatically output code for a given Swift type (e.g. Sourcery) if this gets repetitive.