Advanced Codable types and techniques

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)

6 Likes