Powerful property wrapper to back codable properties.

Overview

BackedCodable

Powerful property wrapper to back codable properties.

Why

Swift's Codable is a great language feature but easily becomes verbose and requires a lot of boilerplate as soon as your serialized files (JSON, Plist) differ from the model you actually want for your app.

BackedCodable offers a single property wrapper to annotate your properties in a declarative way, instead of the good old imperative init(from decoder: Decoder).

Other libraries solve Decodable issues using property wrappers as well, but IMO they are limited by the fact you can apply only one property wrapper per property. So for example, you have to choose between @LossyArray and @DefaultEmptyArray.

With this library, you'll be able to write things like @Backed(Path("attributes", "dates"), options: .lossy, strategy: .secondsSince1970) to decode a lossy array of dates using a seconds since 1970 strategy at the key dates of the nested dictionary attributes.

Installation

BackedDecodable is installable using the Swift Package Manager or CocoaPods.

Usage

  • Mark all properties of your model with @Backed()
  • Make your model conform to BackedDecodable ; it just requires a init(_:DeferredDecoder)

Features

A single @Backed property wrapper provides you all the following features.

Custom decoding path:

@Backed() // key is inferred from property name: "firstName"
var firstName: String 

@Backed("first_name") // custom key 
var firstName: String

@Backed(Path("attributes", "first_name")) // key "first_name" nested in "attributes" dictionary 
var firstName: String

@Backed(Path("attributes", "first_name") ?? "first_name")  // will try "attributes.first_name" and if it fails "first_name" 
var firstName: String

A Path is composed of different PathComponent:

  • .key(String): also expressible by a String literal (Path("foo") == Path(.key("foo")))
  • .index(Int): also expressible by an Integer literal (Path("foo", 0) == Path(.key("foo"), .index(0)))
  • .allKeys: get all keys from a dictionary
  • .allValues: get all values from a dictionary
  • .keys(where: { key, value -> Bool }): filter elements of a dictionary and extract their keys
  • .values(where: { key, value -> Bool }): filter elements of a dictionary and extract their values

Lossy collections filter out invalid or null items and keep only what success. It's a kind of .compactMap().

@Backed(options: .lossy) 
var items: [Item]

@Backed(options: .lossy) 
var tags: Set<String>

Default values for when a key is missing, value is null of value isn't in the right format:

@Backed(defaultValue: .unknown) 
var itemType: ItemType

`@Backed() // defaultValue is automatically set to `nil` so decoding an optional never "fails" 
var name: String? 

Custom date decoding strategy per property:

@Backed("start_date", strategy: .secondsFrom1970) 
var startDate: Date

@Backed("end_date", strategy: .millisecondsFrom1970) 
var endDate: Date

Custom decoder for when a single decoding strategy doesn't stand out:

@Backed("foreground_color", decoder: .HSBAColor) 
var foregroundColor: UIColor

@Backed("background_color", decoder: .RGBAColor) 
var backgroundColor: UIColor

Extensions on Decoder to benefit some of the features above:

init(from decoder: Decoder) throws {
    self.id = try decoder.decoder(String.self, at: "uuid")`
    self.title = try decoder.decoder(String.self, at: Path("attributes", "title"))`
    self.tags = try decoder.decoder([String].self, at: Path("attributes", "tags"), options: .lossy)`
}

Example

Given the following JSON:

{
    "name": "Steve",
    "dates": [1613984296, "N/A", 1613984996],
    "values": [12, "34", 56, "78"],
    "attributes": {
        "values": ["12", 34, "56", 78],
        "all dates": {
            "start_date": 1613984296000,
            "end_date": 1613984996
        }
    },
    "counts": {
        "apples": 12,
        "oranges": 9,
        "bananas": 6
    },
    "foreground_color": {
        "hue": 255,
        "saturation": 128,
        "brightness": 128
    },
    "background_color": {
        "red": 255,
        "green": 128,
        "blue": 128
    },
    "birthdays": {
        "Steve Jobs": -468691200,
        "Tim Cook": -289238400
    }
}

All of this is possible:

public struct BackedStub: BackedDecodable, Equatable {
    public init(_:DeferredDecoder) {}

    @Backed()
    public var someString: String?

    @Backed()
    public var someArray: [String]?

    @Backed()
    public var someDate: Date?

    @Backed(strategy: .secondsSince1970)
    public var someDateSince1970: Date?

    @Backed("full_name" ?? "name" ?? "first_name")
    public var name: String

    @Backed(Path("attributes", "all dates", "start_date"), strategy: .deferredToDecoder)
    public var startDate: Date

    @Backed(Path("attributes", "all dates", "end_date"), strategy: .secondsSince1970)
    public var endDate: Date

    @Backed("dates", options: .lossy, strategy: .secondsSince1970)
    public var dates: [Date]

    @Backed("values", defaultValue: [], options: .lossy)
    public var values: [String]

    @Backed(Path("attributes", "values"), options: .lossy)
    public var nestedValues: [String]?

    @Backed(Path("attributes", "values", 1))
    public var nestedInteger: Int

    @Backed(Path("counts", .allKeys), options: .lossy)
    public var fruits: [Fruits]

    @Backed(Path("counts", .allValues))
    public var counts: [Int]

    @Backed(Path("counts", .allKeys, 0))
    public var bestFruit: String

    @Backed(Path("counts", .allValues, 2))
    public var lastCount: Int

    @Backed(Path("counts", .keys(where: hasSmallCount)))
    public var smallCountFruits: [String]

    @Backed(Path("counts", .keys(where: hasSmallCount), 0))
    public var firstSmallCountFruit: String

    @Backed("foreground_color", decoder: .HSBAColor)
    public var foregroundColor: Color

    @Backed("background_color", decoder: .RGBAColor)
    public var backgroundColor: Color

    @Backed(Path("birthdays", .allValues), strategy: .secondsSince1970)
    public var birthdays: [Date]

    @Backed(Path("birthdays", .allValues, 1), strategy: .secondsSince1970)
    public var timCookBirthday: Date
}

FAQ

How do I declare a memberwise initializer?
struct User: BackedDecodable {
    init(_: DeferredDecoder) {} // required by BackedDecodable
    
    init(id: String, firstName: String, lastName: String) {
        self.$id = id
        self.$firstName = firstName
        self.$lastName = lastName
    }
    
    @Backed("uuid")
    var id: String

    @Backed(Path("attributes", "first_name"))
    var firstName: String
    
    @Backed(Path("attributes", "last_name"))
    var lastName: String
}
What happen if I forgot to set a `$property` in a custom .init(...)? Unfortunately, unless the property is Optional, it will crash. To avoid the crash, must be sure to set all self.$property in all your custom .init(...) like in the memberwise .init(...) example above. This is a known limitation for which I don't have any solution.
Do I need to have all my model backed by BackedDecodable? No! Backed model works on their own and can be composed of plain Decodable properties.
What about performances? I didn't run any performance testing yet (it's on the todo list 😉 ) but as the library uses reflection and go through nested containers from the root Decoder for each properties, you might notice some performance issues. Feel free to open an issue with attached details if you do! 🙏
Wouldn't it be better if all of these was part of Swift? It would! I had to accept some performance and compile-time safety trade-offs to make this library (see above) that probably wouldn't be needed if this was possible in plain Swift. But luckily, Swift is an incredible community driven language, and the core team initiated a discussion around this topic. Check it out: https://forums.swift.org/t/serialization-in-swift/46641

To-do

  • Performance testing
  • Encodable support
  • Mutable properties support
  • Data strategies

Thanks

Author

Jérôme Alves

License

BackedCodable is available under the MIT license. See the LICENSE file for more info.

You might also like...
ExtraLottie is a SwiftUI wrapper of Lottie-iOS

📝 What is ExtraLottie? ExtraLottie is a SwiftUI wrapper of Lottie-iOS. ℹ️ Info Currently ExtraLottie supports custom loop mode, LottieLoopMode, start

SwiftyXPC - a wrapper for Apple’s XPC interprocess communication library that gives it an easy-to-use, idiomatic Swift interface.

SwiftyXPC is a wrapper for Apple’s XPC interprocess communication library that gives it an easy-to-use, idiomatic Swift interface.

Helps you define secure storages for your properties using Swift property wrappers.

🔐 Secure Property Storage Helps you define secure storages for your properties using Swift property wrappers. 🌟 Features All keys are hashed using S

Easily validate your Properties with Property Wrappers 👮
Easily validate your Properties with Property Wrappers 👮

ValidatedPropertyKit enables you to easily validate your properties with the power of Property Wrappers. struct LoginView: View { @Validated(

Codable, but with Super power made custom Codable behavior easy.

Codable, but with Super power made custom Codable behavior easy.

A collection of Swift Property Wrappers (formerly "Property Delegates")

🌯 🌯 Burritos A collection of well tested Swift Property Wrappers. @AtomicWrite @Clamping @Copying @DefaultValue @DynamicUIColor @EnvironmentVariable

🌸 Powerful Codable API requests builder and manager for iOS.
🌸 Powerful Codable API requests builder and manager for iOS.

This lib is about network requests with blackjack, roulette and craps! Using it you will be able to convert your massive API layer code into an awesom

Server-side Swift. The Perfect core toolset and framework for Swift Developers. (For mobile back-end development, website and API development, and more…)
Server-side Swift. The Perfect core toolset and framework for Swift Developers. (For mobile back-end development, website and API development, and more…)

Perfect: Server-Side Swift 简体中文 Perfect: Server-Side Swift Perfect is a complete and powerful toolbox, framework, and application server for Linux, iO

Server-side Swift. The Perfect core toolset and framework for Swift Developers. (For mobile back-end development, website and API development, and more…)
Server-side Swift. The Perfect core toolset and framework for Swift Developers. (For mobile back-end development, website and API development, and more…)

Perfect: Server-Side Swift 简体中文 Perfect: Server-Side Swift Perfect is a complete and powerful toolbox, framework, and application server for Linux, iO

JSON to Core Data and back. Swift Core Data Sync.
JSON to Core Data and back. Swift Core Data Sync.

Notice: Sync was supported from it's creation back in 2014 until March 2021 Moving forward I won't be able to support this project since I'm no longer

Flash Back!
Flash Back!

Flashback - iOS手势返回 效果图 前言 iOS的侧滑手势返回很难用有木有,而且只能从左侧返回,因为不是系统级别,也不是强制使用,还有很多App还不支持,只能羡慕Android的手势返回。为了解决该问题而制作的该库,还是希望苹果有一天能够带来系统级别的手势返回。 Demo To run

From JSON to Core Data and back.
From JSON to Core Data and back.

Groot Groot provides a simple way of serializing Core Data object graphs from or into JSON. It uses annotations in the Core Data model to perform the

Server-side Swift. The Perfect core toolset and framework for Swift Developers. (For mobile back-end development, website and API development, and more…)

Perfect: Server-Side Swift 简体中文 Perfect: Server-Side Swift Perfect is a complete and powerful toolbox, framework, and application server for Linux, iO

Want to know the current weather around the globe? Clima has your back!
Want to know the current weather around the globe? Clima has your back!

Clima (a weather app) Dreaming about going on vacation somewhere? Use Clima to find real time weather from around the world or use your GPS to get loc

CloudKit, Apple’s remote data storage service, provides a possibility to store app data using users’ iCloud accounts as a back-end storage service.
CloudKit, Apple’s remote data storage service, provides a possibility to store app data using users’ iCloud accounts as a back-end storage service.

CloudKit, Apple’s remote data storage service, provides a possibility to store app data using users’ iCloud accounts as a back-end storage service. He

Allows trendy transitions using swipe gesture such as
Allows trendy transitions using swipe gesture such as "swipe back anywhere".

SwipeTransition allows trendy transitions using swipe gesture such as "swipe back". Try the demo on the web (appetize.io): https://appetize.io/app/peb

A Realm-like dynamic property wrapper that maps to APFS extended file attributes.
A Realm-like dynamic property wrapper that maps to APFS extended file attributes.

TOFileAttributes TOFileAttributes is an abstract class that can be extended to enable reading and writing custom data to a local file's extended attri

Backports the new @Invalidating property wrapper to older platforms
Backports the new @Invalidating property wrapper to older platforms

ViewInvalidating A property wrapper that backports the new @Invalidating property wrapper to older versions of iOS/tvOS/macOS. For more information on

Demonstration code for a simple Swift property-wrapper, keypath-based dependency injection system. The keypaths ensure compile-time safety for all injectable services.

Injectable Demo Preliminary musings and demonstration code for a simple Swift property-wrapper, keypath-based dependency injection system. The keypath

Comments
  • Move the required init from BackedDecodable to a static var or function

    Move the required init from BackedDecodable to a static var or function

    Idea

    Having to declare an empty init() might lead to a crash as mentioned in the Readme. It might be clearer to move this requirement to a static variable or function, which clear statement that it should not be used and is only a protocol requirement.

    Examples

    public protocol BackedDecodable: Decodable {
    
        /// Requirement for the `BackedProtocol` when decoding. Should not be used
        //// to instantiate a new value.
        static var emptyBacked: Self
       // or
       static var _emptyBacked: Self
       // or
       static func _emptyBacked() -> Self
    }
    

    So the example User in Readme could become:

    struct User: BackedDecodable {
    
        private init() {}
        static func _emptyBack() -> Self { User() }
    
        init(id: String, firstName: String, lastName: String) {
            self.$id = id
            self.$firstName = firstName
            self.$lastName = lastName
        }
        
        @Backed("uuid")
        var id: String
    
        @Backed(Path("attributes", "first_name"))
        var firstName: String
        
        @Backed(Path("attributes", "last_name"))
        var lastName: String
    }
    

    Miscellaneous

    It's clear in the examples that this would require to make BackedDecodable having an associated type, which has its complexity. Meanwhile, it does appear that it's not required in the library to use objects of types BackedDecodable so this might not be a problem.

    opened by ABridoux 1
Owner
Jérôme Alves
iOS Software Engineer at @DataDog • Previously: @Heetch @Finalcad @deezer @Viseo • Speaker @FrenchKit 2019/2020
Jérôme Alves
A property wrapper to enforce that closures are called exactly once!

A property wrapper that allows you to enforce that a closure is called exactly once. This is especially useful after the introduction of SE-0293 which makes it legal to place property wrappers on function and closure parameters.

Suyash Srijan 11 Nov 13, 2022
A Collection of PropertyWrappers to make custom Serialization of Swift Codable Types easy

CodableWrappers Simplified Serialization with Property Wrappers Move your Codable and (En/De)coder customization to annotations! struct YourType: Coda

null 393 Jan 5, 2023
SafeDecoder - a swift package that set defaults when Codable fails to decode a field

SafeDecoder is a swift package that set defaults when Codable fails to decode a field. SafeDecoder supports configurable default values, See SafeDecoder.Configuration.

GodL 4 Mar 21, 2022
A property finder application written using React Native

React Native PropertyFinder App This repository accompanies the tutorial I published on Ray Wenderlich's website, which describes the process of build

Colin Eberhardt 276 Aug 14, 2022
ProximitySensor - Property wrappers for using the Proximity Sensor from the SwiftUI app

ProximitySensor Property wrappers for using the Proximity Sensor from the SwiftU

null 2 Aug 20, 2022
Runtime Mobile Security (RMS) 📱🔥 - is a powerful web interface that helps you to manipulate Android and iOS Apps at Runtime

Runtime Mobile Security (RMS) ?? ?? by @mobilesecurity_ Runtime Mobile Security (RMS), powered by FRIDA, is a powerful web interface that helps you to

Mobile Security 2k Dec 29, 2022
The most powerful Event-Driven Observer Pattern solution the Swift language has ever seen!

Event-Driven Swift Decoupling of discrete units of code contributes massively to the long-term maintainability of your project(s). While Observer Patt

Flowduino 4 Nov 14, 2022
A powerful, beautiful way to experience Formula1

F1 Pocket Companion A powerful, beautiful way to experience Formula1, right on your iPhone Note This project will probably change it's name. I'm curre

Liam Doyle 3 Nov 5, 2022
A Swift wrapper around the CoreSymbolication private framework on macOS.

CoreSymbolication provides a very powerful system for looking up and extracting symbolic information from mach-o executables, dyld shared caches, and dSYMs.

Stacksift 7 Nov 21, 2022
A Swift wrapper around the JSONbin.io API

A Swift wrapper around the JSONbin.io API

Fleuronic 5 Dec 16, 2021