A common problem I’ve encountered when working with Swift is I want my model to look like this, the JSON looks like that, and I don’t want to have to write and maintain a manual Codable implementation to convert between one and the other. Well, I think I’ve found a solution! Advanced Codable is a cluster of techniques for decoding and encoding difficult payloads, centered on two protocols, Advanced Decodable and Advanced Encodable. They’re pretty simple:
public protocol AdvancedDecodable: Decodable {
associatedtype Decoded: Decodable
init(from decoded: Decoded) throws
}
public protocol AdvancedEncodable: Encodable {
associatedtype Encoded: Encodable
func encode() throws -> Encoded
}
Each comes with an extension:
extension AdvancedDecodable {
public init(from decoder: any Decoder) throws {
let container = try decoder.singleValueContainer()
let decoded = try container.decode(Decoded.self)
try self.init(from: decoded)
}
}
extension AdvancedEncodable {
public func encode(to encoder: any Encoder) throws {
var container = encoder.singleValueContainer()
let encoded = try encode()
try container.encode(encoded)
}
}
The big idea is decoding to an intermediate type, then using an initializer you provide to transform the intermediate into the final type. It’s all wrapped up inside the provided Decodable implementation, so it composes perfectly. As far as the rest of your code is concerned, any AdvancedDecodable type is just Decodable.
Give it a try! I have a longer write-up and a gist, both of which also include a bunch of helper types that work nicely with the intermediates.
Edited to add two worked examples (from the gist):
struct FirstExample: AdvancedCodable {
var a: String
var b: Int
var c: Bool
var d: [String]
init(from decoded: Decoded) throws {
self.a = decoded.a ?? ""
self.b = decoded.b ?? 0
self.c = decoded.c ?? false
self.d = decoded.d?.compactMap(\.value) ?? []
}
func encode() throws -> Encoded {
Encoded(a: a, b: b, c: c, d: d.map(Maybe.value))
}
struct Decoded: Codable {
let a: String?
let b: Int?
let c: Bool?
let d: [Maybe<String>]?
}
}
struct SecondExample: AdvancedCodable {
var strings: [String]
var bools: [Bool]
var ints: [Int]
var doubles: [Double]
init(from decoded: Decoded) throws {
let a = decoded.a?.compactMap(\.value) ?? []
self.strings = a.compactMap(\.left)
self.bools = a.compactMap(\.right).compactMap(\.left)
self.ints = a.compactMap(\.right).compactMap(\.right).compactMap(\.left)
self.doubles = a.compactMap(\.right).compactMap(\.right).compactMap(\.right)
}
func encode() throws -> Encoded {
let strings = strings.map {
Either<String, Either<Bool, Either<Int, Double>>>.left($0)
}
let bools = bools.map {
Either<String, Either<Bool, Either<Int, Double>>>.right(.left($0))
}
let ints = ints.map {
Either<String, Either<Bool, Either<Int, Double>>>.right(.right(.left($0)))
}
let doubles = doubles.map {
Either<String, Either<Bool, Either<Int, Double>>>.right(.right(.right($0)))
}
let combined = strings + bools + ints + doubles
return Encoded(a: combined.map(Maybe.value))
}
struct Decoded: Codable {
let a: [Maybe<Either<String, Either<Bool, Either<Int, Double>>>>]?
}
}
let exampleOneSample = Data("""
{
"a": "String",
"c": false,
"d": ["first", [], "second"]
}
""".utf8)
let exampleTwoSample = Data("""
{
"a": ["first", null, true, 3, 4.1]
}
""".utf8)