3

Being a fan of value types and pure functions I let all my models be structs. But some of my models share properties that I need to JSON encode. Since I cannot use inheritance (mostly good, but in this case bad, but I still wanna stick to structs) I need to declare these properties each struct. But Swift's Codable does not allow for

Using Swift 5.1, is there any elegant solution (as in avoiding code duplication and retorting to NSJSONSerialization and dictionary hacks) for sharing JSON key values for structs? Especially when encoding structs.

Simple example

protocol SerializableModel: Codable {
    static var version: Int { get }
    static var serializerName: String { get }
}

extension SerializableModel {
    // Default value
    static var version: Int { 100 }

    // Static -> Instance convenience
    var version: Int { Self.version }
    var serializerName: String { Self.serializerName }
}

enum SharedCodingKeyValues {}
extension SharedCodingKeyValues {
    static let version = "version"
    static let serializer = "serializer"
}

struct Person {
    let name: String
}

extension Person: SerializableModel {
    static var serializerName: String = "com.mycompany.person"
}

// MARK: Encodable
extension Person {
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)

        // Does not even compile: `Cannot invoke 'encode' with an argument list of type '(Int, forKey: String)'`
//        try container.encode(version, forKey: SharedCodingKeyValues.version)

        // Runtime crash: => `Unexpectedly found nil while unwrapping an Optional value`
//        try container.encode(version, forKey: CodingKeys.init(stringValue: SharedCodingKeyValues.version)!)
//        try container.encode(serializerName, forKey: CodingKeys.init(stringValue: SharedCodingKeyValues.serializer)!)
    }
}

Bah! Okay so that did not work, currently, I have surrendered to this solution which I'm unhappy with:


struct Person {
    let name: String
}

extension Person: SerializableModel {
    static var serializerName: String = "com.mycompany.person"
}

// MARK: CodingKeys
extension Person {

    // Uh, terrible! I REALLY do not wanna do this, because for large models with many stored properties I have to declare ALL my
    // JSON keys, even if they are unchanged (waiting for Swift to improve specifically this point)
    // Also since just be declaring this, auto-synthesizing of `init(from decoder: Decoder)` stopped working, uhh! This is terrible.
    enum CodingKeys: String, CodingKey {
        case name

        // Does not even compile: `Raw value for enum case must be a literal`
//        case serializer = SharedCodingKeyValues.serializer

        case serializer // uh, terrible, the name of this enum has to match ALL my other models' CodingKeys.serializer value, and if the JSON key changes server side I need to find and update them all, since I cannot share this value
        case version
    }
}

// MARK: Encodable
extension Person {

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)

        try container.encode(version, forKey: .version)
        try container.encode(serializerName, forKey: .serializer)
    }
}

// MARK: Decodable
extension Person {
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.name = try container.decode(String.self, forKey: .name)
        // Actually for now we don't care about any incoming values for the keys `version` and `serializerName` has already been used elsewhere
    }
}

So many things about this solution is bad, read the comments inlined with the code.

How can we do better?

Sajjon
  • 8,938
  • 5
  • 60
  • 94

1 Answers1

1

What you can do is define a custom CodingKey that essentially combines both your model's coding keys and any other coding keys you want, like so (warning - there's a lot of code here, but I made it so it's generic enough to be reusable for any model in any project):

protocol CompoundableCodingKey: CodingKey {
    associatedtype OtherCodingKeys1
    associatedtype OtherCodingKeys2

    init(otherCodingKeys1: OtherCodingKeys1)
    init(otherCodingKeys2: OtherCodingKeys2)
    init(intValue: Int)
    init(stringValue: String)
}

struct CompoundCodingKeys<OtherCodingKeys1: CodingKey, OtherCodingKeys2: CodingKey>: CompoundableCodingKey {
    private let otherCodingKeys1: OtherCodingKeys1?
    private let otherCodingKeys2: OtherCodingKeys2?
    private let internalIntValue: Int?
    private let internalStringValue: String

    var intValue: Int? {
        if let otherCodingKeys1 = otherCodingKeys1 {
            return otherCodingKeys1.intValue
        } else if let otherCodingKeys2 = otherCodingKeys2 {
            return otherCodingKeys2.intValue
        }

        return internalIntValue
    }

    var stringValue: String {
        if let otherCodingKeys1 = otherCodingKeys1 {
            return otherCodingKeys1.stringValue
        } else if let otherCodingKeys2 = otherCodingKeys2 {
            return otherCodingKeys2.stringValue
        }

        return internalStringValue
    }

    init(intValue: Int) {
        if let otherCodingKeys1 = OtherCodingKeys1(intValue: intValue) {
            self.otherCodingKeys1 = otherCodingKeys1
            otherCodingKeys2 = nil
            internalIntValue = nil
            internalStringValue = otherCodingKeys1.stringValue
        } else if let otherCodingKeys2 = OtherCodingKeys2(intValue: intValue) {
            otherCodingKeys1 = nil
            self.otherCodingKeys2 = otherCodingKeys2
            internalIntValue = nil
            internalStringValue = otherCodingKeys2.stringValue
        } else {
            otherCodingKeys1 = nil
            otherCodingKeys2 = nil
            internalIntValue = intValue
            internalStringValue = intValue.description
        }
    }

    init(stringValue: String) {
        if let otherCodingKeys1 = OtherCodingKeys1(stringValue: stringValue) {
            self.otherCodingKeys1 = otherCodingKeys1
            otherCodingKeys2 = nil
            internalIntValue = nil
            internalStringValue = otherCodingKeys1.stringValue
        } else if let otherCodingKeys2 = OtherCodingKeys2(stringValue: stringValue) {
            otherCodingKeys1 = nil
            self.otherCodingKeys2 = otherCodingKeys2
            internalIntValue = nil
            internalStringValue = otherCodingKeys2.stringValue
        } else {
            otherCodingKeys1 = nil
            otherCodingKeys2 = nil
            internalIntValue = nil
            internalStringValue = stringValue
        }
    }

    init(otherCodingKeys1: OtherCodingKeys1) {
        self.init(stringValue: otherCodingKeys1.stringValue)
    }

    init(otherCodingKeys2: OtherCodingKeys2) {
        self.init(stringValue: otherCodingKeys2.stringValue)
    }
}

extension KeyedEncodingContainerProtocol where Key: CompoundableCodingKey {
    mutating func encode<T>(_ value: T, forKey key: Key.OtherCodingKeys1) throws where T: Encodable {
        try encode(value, forKey: Key(otherCodingKeys1: key))
    }

    mutating func encode<T>(_ value: T, forKey key: Key.OtherCodingKeys2) throws where T: Encodable {
        try encode(value, forKey: Key(otherCodingKeys2: key))
    }

    mutating func encodeIfPresent<T>(_ value: T?, forKey key: Key.OtherCodingKeys1) throws where T: Encodable {
        try encodeIfPresent(value, forKey: Key(otherCodingKeys1: key))
    }

    mutating func encodeIfPresent<T>(_ value: T?, forKey key: Key.OtherCodingKeys2) throws where T: Encodable {
        try encodeIfPresent(value, forKey: Key(otherCodingKeys2: key))
    }

    mutating func encodeConditional<T>(_ object: T, forKey key: Key.OtherCodingKeys1) throws where T: AnyObject, T: Encodable {
        try encodeConditional(object, forKey: Key(otherCodingKeys1: key))
    }

    mutating func encodeConditional<T>(_ object: T, forKey key: Key.OtherCodingKeys2) throws where T: AnyObject, T: Encodable {
        try encodeConditional(object, forKey: Key(otherCodingKeys2: key))
    }
}

extension KeyedEncodingContainerProtocol where Key: CompoundableCodingKey, Key.OtherCodingKeys1: CompoundableCodingKey {
    mutating func encode<T>(_ value: T, forKey key: Key.OtherCodingKeys1.OtherCodingKeys1) throws where T: Encodable {
        try encode(value, forKey: Key.OtherCodingKeys1(otherCodingKeys1: key))
    }

    mutating func encode<T>(_ value: T, forKey key: Key.OtherCodingKeys1.OtherCodingKeys2) throws where T: Encodable {
        try encode(value, forKey: Key.OtherCodingKeys1(otherCodingKeys2: key))
    }

    mutating func encodeIfPresent<T>(_ value: T?, forKey key: Key.OtherCodingKeys1.OtherCodingKeys1) throws where T: Encodable {
        try encodeIfPresent(value, forKey: Key.OtherCodingKeys1(otherCodingKeys1: key))
    }

    mutating func encodeIfPresent<T>(_ value: T?, forKey key: Key.OtherCodingKeys1.OtherCodingKeys2) throws where T: Encodable {
        try encodeIfPresent(value, forKey: Key.OtherCodingKeys1(otherCodingKeys2: key))
    }

    mutating func encodeConditional<T>(_ object: T, forKey key: Key.OtherCodingKeys1.OtherCodingKeys1) throws where T: AnyObject, T: Encodable {
        try encodeConditional(object, forKey: Key.OtherCodingKeys1(otherCodingKeys1: key))
    }

    mutating func encodeConditional<T>(_ object: T, forKey key: Key.OtherCodingKeys1.OtherCodingKeys2) throws where T: AnyObject, T: Encodable {
        try encodeConditional(object, forKey: Key.OtherCodingKeys1(otherCodingKeys2: key))
    }
}

extension KeyedEncodingContainerProtocol where Key: CompoundableCodingKey, Key.OtherCodingKeys2: CompoundableCodingKey {
    mutating func encode<T>(_ value: T, forKey key: Key.OtherCodingKeys2.OtherCodingKeys1) throws where T: Encodable {
        try encode(value, forKey: Key.OtherCodingKeys2(otherCodingKeys1: key))
    }

    mutating func encode<T>(_ value: T, forKey key: Key.OtherCodingKeys2.OtherCodingKeys2) throws where T: Encodable {
        try encode(value, forKey: Key.OtherCodingKeys2(otherCodingKeys2: key))
    }

    mutating func encodeIfPresent<T>(_ value: T?, forKey key: Key.OtherCodingKeys2.OtherCodingKeys1) throws where T: Encodable {
        try encodeIfPresent(value, forKey: Key.OtherCodingKeys2(otherCodingKeys1: key))
    }

    mutating func encodeIfPresent<T>(_ value: T?, forKey key: Key.OtherCodingKeys2.OtherCodingKeys2) throws where T: Encodable {
        try encodeIfPresent(value, forKey: Key.OtherCodingKeys2(otherCodingKeys2: key))
    }

    mutating func encodeConditional<T>(_ object: T, forKey key: Key.OtherCodingKeys2.OtherCodingKeys1) throws where T: AnyObject, T: Encodable {
        try encodeConditional(object, forKey: Key.OtherCodingKeys2(otherCodingKeys1: key))
    }

    mutating func encodeConditional<T>(_ object: T, forKey key: Key.OtherCodingKeys2.OtherCodingKeys2) throws where T: AnyObject, T: Encodable {
        try encodeConditional(object, forKey: Key.OtherCodingKeys2(otherCodingKeys2: key))
    }
}

extension KeyedDecodingContainerProtocol where Key: CompoundableCodingKey {
    func decode<T>(_ type: T.Type, forKey key: Key.OtherCodingKeys1) throws -> T where T: Decodable {
        try decode(type, forKey: Key(otherCodingKeys1: key))
    }

    func decode<T>(_ type: T.Type, forKey key: Key.OtherCodingKeys2) throws -> T where T: Decodable {
        try decode(type, forKey: Key(otherCodingKeys2: key))
    }

    func decodeIfPresent<T>(_ type: T.Type, forKey key: Key.OtherCodingKeys1) throws -> T? where T: Decodable {
        try decodeIfPresent(type, forKey: Key(otherCodingKeys1: key))
    }

    func decodeIfPresent<T>(_ type: T.Type, forKey key: Key.OtherCodingKeys2) throws -> T? where T: Decodable {
        try decodeIfPresent(type, forKey: Key(otherCodingKeys2: key))
    }
}

extension KeyedDecodingContainerProtocol where Key: CompoundableCodingKey, Key.OtherCodingKeys1: CompoundableCodingKey {
    func decode<T>(_ type: T.Type, forKey key: Key.OtherCodingKeys1.OtherCodingKeys1) throws -> T where T: Decodable {
        try decode(type, forKey: Key.OtherCodingKeys1(otherCodingKeys1: key))
    }

    func decode<T>(_ type: T.Type, forKey key: Key.OtherCodingKeys1.OtherCodingKeys2) throws -> T where T: Decodable {
        try decode(type, forKey: Key.OtherCodingKeys1(otherCodingKeys2: key))
    }

    func decodeIfPresent<T>(_ type: T.Type, forKey key: Key.OtherCodingKeys1.OtherCodingKeys1) throws -> T? where T: Decodable {
        try decodeIfPresent(type, forKey: Key.OtherCodingKeys1(otherCodingKeys1: key))
    }

    func decodeIfPresent<T>(_ type: T.Type, forKey key: Key.OtherCodingKeys1.OtherCodingKeys2) throws -> T? where T: Decodable {
        try decodeIfPresent(type, forKey: Key.OtherCodingKeys1(otherCodingKeys2: key))
    }
}

extension KeyedDecodingContainerProtocol where Key: CompoundableCodingKey, Key.OtherCodingKeys2: CompoundableCodingKey {
    func decode<T>(_ type: T.Type, forKey key: Key.OtherCodingKeys2.OtherCodingKeys1) throws -> T where T: Decodable {
        try decode(type, forKey: Key.OtherCodingKeys2(otherCodingKeys1: key))
    }

    func decode<T>(_ type: T.Type, forKey key: Key.OtherCodingKeys2.OtherCodingKeys2) throws -> T where T: Decodable {
        try decode(type, forKey: Key.OtherCodingKeys2(otherCodingKeys2: key))
    }

    func decodeIfPresent<T>(_ type: T.Type, forKey key: Key.OtherCodingKeys2.OtherCodingKeys1) throws -> T? where T: Decodable {
        try decodeIfPresent(type, forKey: Key.OtherCodingKeys2(otherCodingKeys1: key))
    }

    func decodeIfPresent<T>(_ type: T.Type, forKey key: Key.OtherCodingKeys2.OtherCodingKeys2) throws -> T? where T: Decodable {
        try decodeIfPresent(type, forKey: Key.OtherCodingKeys2(otherCodingKeys2: key))
    }
}

Then, if we change SharedCodingKeyValues into a CodingKey enum, like so:

enum SharedCodingKeys: String, CodingKey {
    case version
    case serializer
}

...we can then get a container keyed by the type CompoundCodingKeys<CodingKeys, SharedCodingKeys>, which lets you specify either Person.CodingKeys (which does not need to be manually defined), or SharedCodingKeys, like so:

// MARK: Encodable
extension Person {
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CompoundCodingKeys<CodingKeys, SharedCodingKeys>.self)

        // From `Person.CodingKeys`
        try container.encode(name, forKey: .name)

        // From `SharedCodingKeys`
        try container.encode(version, forKey: .version)
        try container.encode(serializerName, forKey: .serializer)
    }
}

Since CompoundCodingKeys itself conforms to CodingKey, you can even take this one step further and combine as many coding keys as you want into one, like CompoundCodingKeys<CodingKeys, CompoundCodingKeys<SomeOtherCodingKeys, SharedCodingKeys>>:

enum SomeOtherCodingKeys: String, CodingKey {
    case someOtherKey
}

// MARK: Encodable
extension Person {
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CompoundCodingKeys<CodingKeys, CompoundCodingKeys<SomeOtherCodingKeys, SharedCodingKeys>>.self)

        // From `Person.CodingKeys`
        try container.encode(name, forKey: .name)

        // From `SharedCodingKeys`
        try container.encode(version, forKey: .version)
        try container.encode(serializerName, forKey: .serializer)

        // From `SomeOtherCodingKeys`
        try container.encode(serializerName, forKey: .someOtherKey)
    }
}
TylerP
  • 9,600
  • 4
  • 39
  • 43
  • Hi, sorry to bother you, but do you know if your proposed solution could come in handy for this similar question i have posted here https://stackoverflow.com/questions/66283127/swift-codable-dynamic-keys-as-a-subset-for-shared-properties-across-different-mo?noredirect=1#comment117187097_66283127 – Coder Feb 19 '21 at 22:15