Skip to content

An enhancement of a current codable protocol for easier and better [en/de]coding of JSON.

License

Notifications You must be signed in to change notification settings

rehannali/SGSerializable

This branch is up to date with master.

Repository files navigation

SGSerializable

Enhancement of current codable protocol using property wrappers for easier access and better [en/de]coding of JSON. The goal of these property wrappers is to avoid implementing custom initializer for decoder, encoder, sometimes coding keys and suffer through boilerplate code.

Language: Swift 5 SPM compatible Platforms MIT
Build Status CircleCI codebeat badge DeepSource Release version

Features

SGCodable

  • Typed alias with SGDecodable & SGEncodable
  • Custom [en/de]coding using property wrappers.
  • This protocol is required for use property wrappers mentioned below.

SGSerializable

  • No need to write your own custom init(from decoder: Decoder) throws and func encode(to encoder: Encoder) throws
  • Non need to write custom CodingKeys.
  • Custom Decoding key
  • Use property name if key isn't mentioned.
  • Works with inheritance and composition.
  • Custom Transformer implementation
  • Default transformer classes i.e. StringToURL etc. List is available below.
  • Default Value if decoding failed or not provided. You can provide own default value as well.
@SGSerializable(default: "", key: "your_key", type: .auto)
var key: String?

SGTransformSerializable

  • Custom tranformable classes if required or you can use default ones.
  • Transforming objects to your own type like String to Native URL.

Example

struct Foo: SGCodable {
    @SGSerializable
    var key: String?
    
    @SGSerializable(default: [])
    var items: [String]
    
    @SGSerializable(key: "crypto")
    var cryptography: [String: String]
    
    @SGSerializable
    var values: [Double]?
}
struct Person: SGCodable {
    @SGTransformSerializable<StringToURL>
    var profilePictureURL: URL?
}

Requirements

  • iOS 11.0+ / macOS 10.15+ / tvOS 11.0+ / watchOS 6.0+
  • Xcode 13+
  • Swift 5.5+

Installation

Swift Package Manager

URL: https://github.com/rehannali/SGSerializable.git

Add this to the depedencies in your Package.swift manifest file.

dependencies: [
    .package(url: "https://github.com/rehannali/SGSerializable.git")
]

Usage

  • Mark all properties with SGSerializable.
  • Mark your model class/struct with SGCodable.
  • You can use either SGDecodable or SGEncodable for decoding and encoding respectively.
class Person: SGDecodable {
    @SGSerializable(type: .string)
    var name: String?
    
    @SGSerializable(key: "age")
    var age: Int
    
    @SGSerializable(default: "", key: "city")
    var city: String
    
    @SGSerializable(default: Animal(), key: "animal")
    var animmal: Animal?
    
    required init() {}
}

class Role: Person {
    @SGSerializable
    var role: String
}

struct Animal: SGDecodable {
    @SGSerializable
    var name: String?
}
{
    "name": "John",
    "age": "12",
    "city": "London"
}

Above mentioned JSON model successfully decode into Person class. It will successfully decode age: String into age: Int.

If type is provided, it'll use that type to downcast the value. Default: .auto will be used.

Other available CodingTypes are mentioned below.

CodingType
case int
case double
case float
case bool
case string
case auto
case none

Custom initializer

struct Person: SGCodable {
    init(name: String?) {
        self.$name = name
    }

    @SGSerializable
    var name: String
}

Custom Transformer Implementation

You can create own transormation by conforming to SGTranformable.

struct StringToBool: SGTranformable {
    typealias FromType = String
    typealias ToType = Bool

    static func transform(from value: String?) -> Bool? {
        guard let value = value else { return nil }
        switch value {
            case "true": return true
            default: return false
        }
    }

    static func transform(from value: Bool?) -> String? {
        guard let value = value else { return nil }
        let stringValue = value ? "true" : "false"
        return stringValue
    }
}

struct Person: SGCodable {
    @SGTransformSerializable<StringToBool>
    var isLogin: Bool?
}

You can access optional values by access attributes directly.

let person = Person()
let isLogin = person.isLogin

now isLogin is optional for transform classes. You can access non optional values like person.$isLogin. It can automatically gives you default value of else fatalError if not found because of programming error. Make sure you provide default value for custom objects if you want to access non-optional value.

Custom initializer for tranformable

struct Person: SGCodable {
    init(isLogin: Bool?) {
        self.isLogin = isLogin
    }

    @SGTransformSerializable<StringToBool>
    var isLogin: Bool?
}
Defaut Transformer Classes
StringToBase64Data
StringToDateISO8601
StringToDateRFC3339
TimeIntervalToDate
MilliSecondsToDate
StringToURL

Omit [En/De]coding

There are some cases when you don't want to encode to decode attributes. You can do it by simply add variable and don't use the property wrappers for that attribute.

struct Animal: SGCodable {
    @SGSerializable
    var name: String?

    // This will omit from [en/de]coding
    var omitVariable: String?
}

Classes to Dictionary

There is time we need to add custom function or computed property to provide dictionary from object as we write it manually. Now it is possible by conforming your class to NSObject or NSObjectProtocol and get dictionary with object.swiftDictionary or you can convert any class and struct to dictionary by conforming to SGDictionaryConverter.

class Person: NSObject {
    var name = "Frank"
    var age = -1
    var address: String?
}

let person = Person()
person.swiftDictionary
class Address: SGDictionaryConverter {
    var street: String?
    var city: String?
    var state: String?
    var country: String?
}

class Person: Address {
    var name = "Frank"
    var age = -1
}

let person = Person()
person.toDictionary()
struct Person: SGDictionaryConverter {
    var name = "Frank"
    var age = -1
    var address: Address?
}

struct Address: SGDictionaryConverter {
    var street: String?
    var city: String?
    var state: String?
    var country: String?
}

let person = Person()
person.toDictionary()

Inheritance and Composition works out of the box using SGDictionaryConverter.

Strip Null or Default Values

If you want to omit optional or default values like empty string or nil, you can use strippingNullOrDefaults() to any Array or Dictionary.

let person = Person()
person.swiftDictionary.strippingNullOrDefaults()

it will omit default and nil values which can be useful while performing network request.

End Result:

["name": "Frank"]

Inheritance and Composition works out of the box with swiftDictionary and strippingNullOrDefaults()

Helper Functions for [De/En]coding

Define class/struct using required protocol SGCodable.

struct Person {
    @SGSerializable
    var name: String?
}

Decoable Helpers

let person = try Person.initialize(with: jsonData)
let person = try Person.initialize(with: jsonString)
let person = try Person.initialize(from: jsonURL)

or you can use like this:

let person = try [Person].initialize(with: jsonData)
let person = try [Person].initialize(with: jsonString)
let person = try [Person].initialize(from: jsonURL)

Encodable Helpers

let jsonString = person.jsonString()
let jsonData = person.jsonData()
let dictionary = person.dictionary

Contribute

This is only a tip of the iceberg of what we can achieve using Property Wrappers and how it can be improved [de/en]coding of JSON using property wrappers in Swift. If there is a any custom tranformation that could be added feel free to open an issue requesting it and/or submit a pull request with the new option.

About

An enhancement of a current codable protocol for easier and better [en/de]coding of JSON.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages