/ExCodable

Converts between JSON and Models - An Extension of Swift Codable.

Primary LanguageSwiftMIT LicenseMIT

ExCodable

Swift 5.0 Swift Package Manager Platforms Build and Test GitHub Releases (latest SemVer) Deploy to CocoaPods Cocoapods LICENSE @minglq

En | 中文

Contents

Features

  • Extends Swift Codable - Encodable & Decodable;
  • Supports Key-Mapping via KeyPath and Coding-Key:
    • ExCodable did not read/write memory via unsafe pointers;
    • No need to encode/decode properties one by one;
    • Just requires using var to declare properties and provide default values;
    • In most cases, the CodingKey type is no longer necessary, because it will only be used once, String literals may be better.
  • Supports multiple Key-Mappings for different data sources;
  • Supports multiple Alternative-Keys via Array for decoding;
  • Supports Nested-Keys via String with dot syntax;
  • Supports customized encode/decode via subscripts;
  • Supports builtin and custom Type-Conversions;
  • Supports struct, class and subclass;
  • Supports encode/decode with or without IfPresent;
  • Supports abort (throws error) or continue (returns nil) encode/decode if error encountered;
  • Uses JSON encoder/decoder by default, and supports PList;
  • Uses Type-Inference, supports JSON Data, String and Object.

Usage

0. Codable:

With Codable, it just needs to adop the Codable protocol without implementing any method of it.

struct TestAutoCodable: Codable, Equatable {
    private(set) var int: Int = 0
    private(set) var string: String?
    enum CodingKeys: String, CodingKey {
        case int = "i", string = "s"
    }
}

But, if you have to encode/decode manually for some reason, e.g. Alternative-Keys and Nested-Keys ...

struct TestManualCodable: Equatable {
    private(set) var int: Int = 0
    private(set) var string: String?
}

extension TestManualCodable: Codable {
    
    enum Keys: CodingKey {
        case int, i
        case nested, string
    }
    
    init(from decoder: Decoder) throws {
        if let container = try? decoder.container(keyedBy: Keys.self) {
            if let int = (try? container.decodeIfPresent(Int.self, forKey: Keys.int))
                ?? (try? container.decodeIfPresent(Int.self, forKey: Keys.i)) {
                self.int = int
            }
            if let nestedContainer = try? container.nestedContainer(keyedBy: Keys.self, forKey: Keys.nested),
               let string = try? nestedContainer.decodeIfPresent(String.self, forKey: Keys.string) {
                self.string = string
            }
        }
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: Keys.self)
        try? container.encodeIfPresent(int, forKey: Keys.int)
        var nestedContainer = container.nestedContainer(keyedBy: Keys.self, forKey: Keys.nested)
        try? nestedContainer.encodeIfPresent(string, forKey: Keys.string)
    }
}

With ExCodable:

struct TestExCodable: Equatable {
    private(set) var int: Int = 0
    private(set) var string: String?
}

extension TestExCodable: ExCodable {
    static let keyMapping: [KeyMap<Self>] = [
        KeyMap(\.int, to: "int"),
        KeyMap(\.string, to: "string")
    ]
    init(from decoder: Decoder) throws {
        try decode(from: decoder, with: Self.keyMapping)
    }
}

1. Key-Mapping for struct:

With ExCodable, it needs to to declare properties with var and provide default values.

struct TestStruct: Equatable {
    private(set) var int: Int = 0
    private(set) var string: String?
    var bool: Bool!
}
extension TestStruct: ExCodable {
    
    static let keyMapping: [KeyMap<Self>] = [
        KeyMap(\.int, to: "int"),
        KeyMap(\.string, to: "string"),
    ]
    
    init(from decoder: Decoder) throws {
        try decode(from: decoder, with: Self.keyMapping)
    }
    // `encode` with default implementation can be omitted
    // func encode(to encoder: Encoder) throws {
    //     try encode(to: encoder, with: Self.keyMapping)
    // }
}

2. Alternative-Keys:

static let keyMapping: [KeyMap<Self>] = [
    KeyMap(\.int, to: "int", "i"),
    KeyMap(\.string, to: "string", "str", "s")
]

3. Nested-Keys:

static let keyMapping: [KeyMap<Self>] = [
    KeyMap(\.int, to: "int"),
    KeyMap(\.string, to: "nested.string")
]

4. Custom encode/decode:

struct TestCustomEncodeDecode: Equatable {
    var int: Int = 0
    var string: String?
}
extension TestCustomEncodeDecode: ExCodable {
    
    private enum Keys: CodingKey {
        case int, string
    }
    private static let dddd = "dddd"
    private func string(for int: Int) -> String {
        switch int {
            case 100: return "Continue"
            case 200: return "OK"
            case 304: return "Not Modified"
            case 403: return "Forbidden"
            case 404: return "Not Found"
            case 418: return "I'm a teapot"
            case 500: return "Internal Server Error"
            case 200..<400: return "success"
            default: return "failure"
        }
    }
    
    static let keyMapping: [KeyMap<Self>] = [
        KeyMap(\.int, to: Keys.int),
    ]
    
    init(from decoder: Decoder) throws {
        try decode(from: decoder, with: Self.keyMapping)
        string = decoder[Keys.string]
        if string == nil || string == Self.dddd {
            string = string(for: int)
        }
    }
    func encode(to encoder: Encoder) throws {
        try encode(to: encoder, with: Self.keyMapping)
        encoder[Keys.string] = Self.dddd
    }
}

5. Encode/decode constant properties with subscripts:

Using let to declare properties without default values.

struct TestSubscript: Equatable {
    let int: Int
    let string: String
}
extension TestSubscript: Encodable, Decodable {
    
    enum Keys: CodingKey {
        case int, string
    }
    
    init(from decoder: Decoder) throws {
        // - seealso:
        // string = decoder.decode(<#T##codingKeys: CodingKey...##CodingKey#>)
        // string = try decoder.decodeThrows(<#T##codingKeys: CodingKey...##CodingKey#>)
        // string = try decoder.decodeNonnullThrows(<#T##codingKeys: CodingKey...##CodingKey#>)
        int = decoder[Keys.int] ?? 0
        string = decoder[Keys.string] ?? ""
    }
    func encode(to encoder: Encoder) throws {
        // - seealso:
        // encoder.encode(<#T##value: Encodable?##Encodable?#>, for: <#T##CodingKey#>)
        // try encoder.encodeThrows(<#T##value: Encodable?##Encodable?#>, for: <#T##CodingKey#>)
        // try encoder.encodeNonnullThrows(<#T##value: Encodable##Encodable#>, for: <#T##CodingKey#>)
        encoder[Keys.int] = int
        encoder[Keys.string] = string
    }
}

6. Custom Type-Conversions:

Declare struct FloatToBoolDecodingTypeConverter with protocol ExCodableDecodingTypeConverter and implement its method, decode values in alternative types and convert to target type:

struct FloatToBoolDecodingTypeConverter: ExCodableDecodingTypeConverter {
    public func decode<T: Decodable, K: CodingKey>(_ container: KeyedDecodingContainer<K>, codingKey: K, as type: T.Type) -> T? {
        // Bool -> Double
        if type is Double.Type || type is Double?.Type {
            if let bool = try? container.decodeIfPresent(Bool.self, forKey: codingKey) {
                return (bool ? 1.0 : 0.0) as? T
            }
        }
        // Bool -> Float
        else if type is Float.Type || type is Float?.Type {
            if let bool = try? container.decodeIfPresent(Bool.self, forKey: codingKey) {
                return (bool ? 1.0 : 0.0) as? T
            }
        }
        // Double or Float NOT found
        return nil
    }
}

Register FloatToBoolDecodingTypeConverter with an instance:

register(FloatToBoolDecodingTypeConverter())

7. Key-Mapping for class:

Cannot adopt ExCodable in extension of classes.

class TestClass: ExCodable, Equatable {
    
    var int: Int = 0
    var string: String? = nil
    init(int: Int, string: String?) {
        (self.int, self.string) = (int, string)
    }
    
    static let keyMapping: [KeyMap<TestClass>] = [
        KeyMap(ref: \.int, to: "int"),
        KeyMap(ref: \.string, to: "string")
    ]
    
    required init(from decoder: Decoder) throws {
        try decodeReference(from: decoder, with: Self.keyMapping)
    }
    
    static func == (lhs: TestClass, rhs: TestClass) -> Bool {
        return lhs.int == rhs.int && lhs.string == rhs.string
    }
}

8. Key-Mapping for subclass:

Requires declaring another static Key-Mapping for subclass.

class TestSubclass: TestClass {
    var bool: Bool = false
    required init(int: Int, string: String, bool: Bool) {
        self.bool = bool
        super.init(int: int, string: string)
    }
    
    static let keyMappingForTestSubclass: [KeyMap<TestSubclass>] = [
        KeyMap(ref: \.bool, to: "bool")
    ]
    
    required init(from decoder: Decoder) throws {
        try super.init(from: decoder)
        try decodeReference(from: decoder, with: Self.keyMappingForTestSubclass)
    }
    override func encode(to encoder: Encoder) throws {
        try super.encode(to: encoder)
        try encode(to: encoder, with: Self.keyMappingForTestSubclass)
    }
    
    static func == (lhs: TestSubclass, rhs: TestSubclass) -> Bool {
        return lhs.int == rhs.int
            && lhs.string == rhs.string
            && lhs.bool == rhs.bool
    }
}

9. Encode/decode with Type-Inference:

let test = TestStruct(int: 304, string: "Not Modified")
let data = try? test.encoded() as Data?
let copy1 = try? data?.decoded() as TestStruct?
let copy2 = data.map { try? TestStruct.decoded(from: $0) }
XCTAssertEqual(copy1, test)
XCTAssertEqual(copy2, test)

Requirements

  • iOS 8.0+ | tvOS 9.0+ | macOS X 10.10+ | watchOS 2.0+
  • Xcode 12.0+
  • Swift 5.0+

Installation

.package(url: "https://github.com/iwill/ExCodable", from: "0.6.0")
pod 'ExCodable', '~> 0.6.0'
  • Code Snippets:

Title: ExCodable Summary: Adopte to ExCodable protocol Language: Swift
Platform: All
Completion: ExCodable
Availability: Top Level

<#extension/struct/class#> <#Type#>: ExCodable {
    static let <#keyMapping#>: [KeyMap<<#SelfType#>>] = [
        KeyMap(\.<#property#>, to: <#"key"#>),
        <#...#>
    ]
    init(from decoder: Decoder) throws {
        try decode<#Reference#>(from: decoder, with: Self.<#keyMapping#>)
    }
    func encode(to encoder: Encoder) throws {
        try encode(to: encoder, with: Self.<#keyMapping#>)
    }
}

Like it?

Hope you like this project, don't forget to give it a star

Star Chart

Thanks to

Connect with me

License

ExCodable is released under the MIT license. See LICENSE for details.