Skip to content

ExCodable/ExCodable

Repository files navigation

ExCodable

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

En | 中文

What's New in ExCodable 1.0

  • Uses @propertyWrapper instead of key-mapping, the new syntax is more concise, elegant and easier to use.
  • Optimized type conversions.
  • Fix bugs.

Documentations

Features

struct TestExCodable: ExAutoCodable {
    @ExCodable private(set)
    var int: Int = 0
    @ExCodable("nested.nested.string", "string", "str", "s") private(set)
    var string: String? = nil
}
  • Converts between JSON and models.
  • Based on and Extends Swift Codable.
  • Associates JSON keys to properties via annotations - @propertyWrapper:
    • ExCodable did not read/write memory via unsafe pointers.
    • No need to encode and decode properties one by one.
    • In most cases, the CodingKey types are no longer necessary, String literals are preferred.
    • Currently, requires declaring properties with var and provide default values.
  • Supports alternative keys for decoding.
  • Supports nested keys via String with dot syntax.
  • Supports builtin and custom type conversions, including nested optionals as well.
  • Supports manual encoding/decoding using subscripts.
  • Supports return nil or throw error when encoding/decoding failed.
  • Supports struct, enum, class and subclasses.
  • Supports JSON Data, String, Array and Dictionary.
  • Supports type inference.
  • Uses JSON encoder/decoder by default, supports PList and custom encoder/decoder.
  • Super lightweight.

TODO:

  • Supports let.
  • Supports var without default values.
  • Use Macros instead of protocol ExAutoCodable.

Usage

1. ExCodable

ExCodable requires declaring properties with var and provide default values.

struct TestStruct: ExAutoCodable {
    @ExCodable private(set)
    var int: Int = 0
    @ExCodable("string") private(set)
    var string: String? = nil
}

2. Alternative Keys

struct TestAlternativeKeys: ExAutoCodable {
    @ExCodable("string", "str", "s") private(set)
    var string: String! = nil
}

3. Nested Keys

struct TestNestedKeys: ExAutoCodable {
    @ExCodable("nested.nested.string") private(set)
    var string: String! = nil
}

4. enum

enum TestEnum: Int, Codable {
    case zero = 0, one = 1
}

struct TestStructWithEnum: ExAutoCodable {
    @ExCodable("enum") private(set)
    var `enum` = TestEnum.zero
}

5. Custom Encoding/Decoding

struct TestManualEncodeDecode: ExAutoCodable {
    @ExCodable(encode: { encoder, value in
        encoder["int"] = value <= 0 ? 0 : value
    }, decode: { decoder in
        if let int: Int = decoder["int"], int > 0 {
            return int
        }
        return 0
    }) private(set)
    var int: Int = 0
}

6. Type Conversions

ExCodable builtin type conversions:

  • boolean:
    • Bool
    • Int, Int8, Int16, Int32, Int64
    • UInt, UInt8, UInt16, UInt32, UInt64
    • String
  • integer:
    • Bool
    • Int, Int8, Int16, Int32, Int64
    • UInt, UInt8, UInt16, UInt32, UInt64
    • Double, Float
    • String
  • float:
    • Int, Int8, Int16, Int32, Int64
    • UInt, UInt8, UInt16, UInt32, UInt64
    • Double, Float
    • String
  • string:
    • Bool
    • Int, Int8, Int16, Int32, Int64
    • UInt, UInt8, UInt16, UInt32, UInt64
    • Double, Float
    • String

Custom type conversions for specific properties:

struct TestCustomEncodeDecode: ExAutoCodable {
    @ExCodable(decode: { decoder in
        if let string: String = decoder["string"] {
            return string.count
        }
        return 0
    }) private(set)
    var int: Int = 0
}

Custom type conversions for specific model:

struct TestCustomTypeConverter: ExAutoCodable {
    @ExCodable private(set)
    var doubleFromBool: Double? = nil
    @ExCodable private(set)
    var floatFromBool: Double? = nil
}

extension TestCustomTypeConverter: ExCodableDecodingTypeConverter {
    
    public static func decode<T: Decodable, K: CodingKey>(_ container: KeyedDecodingContainer<K>, codingKey: K, as type: T.Type) -> T? {
        
        // for nested optionals, e.g. `var int: Int??? = nil`
        let wrappedType = T?.wrappedType
        
        // decode Double from Bool
        if type is Double.Type || wrappedType is Double.Type {
            if let bool = try? container.decodeIfPresent(Bool.self, forKey: codingKey) {
                return (bool ? 1.0 : 0.0) as? T
            }
        }
        // decode Float from Bool
        else if type is Float.Type || wrappedType is Float.Type {
            if let bool = try? container.decodeIfPresent(Bool.self, forKey: codingKey) {
                return (bool ? 1.0 : 0.0) as? T
            }
        }
        
        return nil
    }
}

Custom type conversions for global:

struct TestCustomGlobalTypeConverter: ExAutoCodable, Equatable {
    @ExCodable("boolFromDouble") private(set)
    var boolFromDouble: Bool? = nil
}

extension ExCodableGlobalDecodingTypeConverter: ExCodableDecodingTypeConverter {
    
    public static func decode<T: Decodable, _K: CodingKey>(_ container: KeyedDecodingContainer<_K>, codingKey: _K, as type: T.Type) -> T? {
        
        // for nested optionals, e.g. `var int: Int??? = nil`
        let wrappedType = T?.wrappedType
        
        // decode Bool from Double
        if type is Bool.Type || wrappedType is Bool.Type {
            if let double = try? container.decodeIfPresent(Double.self, forKey: codingKey) {
                return (double != 0) as? T
            }
        }
        
        return nil
    }
}

7. Manual Encoding/Decoding using Subscripts

A type without ExCodable:

struct TestManualEncodingDecoding {
    let int: Int
    let string: String
}

Manual encoding/decoding using subscripts:

extension TestManualEncodingDecoding: Codable {
    
    enum Keys: CodingKey {
        case int, string
    }
    
    init(from decoder: Decoder) throws {
        int = decoder[Keys.int] ?? 0
        string = decoder[Keys.string] ?? ""
    }
    func encode(to encoder: Encoder) throws {
        encoder[Keys.int] = int
        encoder[Keys.string] = string
    }
}

8. return nil or throw error - UNSTABLE

While encoding/decoding, ExCodable ignores the keyNotFound, valueNotFound, invalidValue and typeMismatch errors and return nil by default, only throws JSON errors.

ExCodable also supports throw errors:

  • Use nonnull: true to throw EncodingError.invalidValue, DecodingError.keyNotFound, DecodingError.valueNotFound.
  • Use throws: true to throw DecodingError.typeMismatch.
struct TestNonnullAndThrows: ExAutoCodable {
    @ExCodable(nonnull: true, throws: true) private(set)
    var int: Int! = 0
}

9. class and Subclasses

class TestClass: ExAutoCodable {
    @ExCodable private(set)
    var int: Int = 0
    @ExCodable private(set)
    var string: String? = nil
    required init() {}
    init(int: Int, string: String?) {
        (self.int, self.string) = (int, string)
    }
}
class TestSubclass: TestClass {
    @ExCodable private(set)
    var bool: Bool = false
    required init() { super.init() }
    required init(int: Int, string: String, bool: Bool) {
        self.bool = bool
        super.init(int: int, string: string)
    }
}

10. Type Inference

struct TestStruct: ExAutoCodable, Equatable {
    @ExCodable private(set)
    var int: Int = 0
    @ExCodable private(set)
    var string: String? = nil
}

let json = Data(#"{"int":200,"string":"OK"}"#.utf8)
let model = try? TestStruct.decoded(from: json)

let dict = try? model.encoded() as [String: Any]
let copy = try? dict.decoded() as TestStruct

See the tests for more examples.

Requirements

  • iOS 12.0+ | tvOS 12.0+ | macOS 11.0+ | watchOS 4.0+
  • Xcode 15.4+
  • Swift 5.10+

Installation

Swift Package Manager:

.package(url: "https://github.com/ExCodable/ExCodable", from: "1.0.0")

CocoaPods:

pod 'ExCodable', '~> 1.0.0'

Migration

0.x to 1.x

When you update to ExCodable 1.0.

Step 1: Update your code to use the old API - DEPRECATED but quick.

  • Replace protocol ExCodable with ExCodableDEPRECATED.
  • Add static to func decodeForTypeConversion(_:codingKey:as:) of protocol KeyedDecodingContainerCustomTypeConversion.
struct TestExCodable {
    private(set) var int: Int = 0
    private(set) var string: String?
}

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

Step 2: Upgrade your models to the new API one by one - SUGGESTED:

  • Replace protocol ExCodable with ExAutoCodable.
  • Remove initializer init(from decoder: Decoder) throws.
  • Remove static properties keyMapping.
  • Use @ExCodable("<key>", "<alt-key>", ...).
  • See Usage for more details.
struct TestExCodable: ExAutoCodable {
    @ExCodable private(set)
    var int: Int = 0
    @ExCodable("nested.nested.string", "string", "str", "s") private(set)
    var string: String?
}

Stars

Star Chart

Hope ExCodable will help you! Make a star ⭐️ 🤩

Thanks to

About Me

License

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