Swift Macro enabling safe decoding of structs and classes.
- Allow decoding to recover from partial failures:
Optionalproperties to recover from invalid structure (e.g. andInt?property receiving aString)Array,SetandDictionaryproperties to recover from invalid items (Dictionaryis limited to recovering from invalidValues)- per-property opt-out using
@IgnoreSafeDecodingmacro in properties - plays well with (actually, ignores) computed and initialized properties
- Automatic conformance to
Decodable(if needed)
- Requires Swift 5.9
- Add
@SafeDecoding(decodingStrategy:)and@SafeDecoding(decodingStrategy:reporter:)forenums - Add
EnumCaseDecodingStrategyto describe decoding strategy forenums
- Add
@RetryDecoding, allowing individual properties to have associated retries - Add
@FallbackDecoding, allowing individual properties to provide a last-resort value
- Bug: uses
decodeIfPresentfor optionals when using error reporting
- Accounts for access modifiers
- Add error reporting to
@SafeDecodingmacro - Remove sample client product
- Add
@SafeDecodingand@IgnoreSafeDecodingmacros
A common problem is when an otherwise non-mandatory part of a model contains invalid/missing data, causing an entire payload to fail.
In the following example we have a Book model, for which we'll retrieve an array from some backend.
struct Tag: Decodable {
let name: String
}
struct Book: Decodable {
let title: String
let author: String
let synopsis: String?
let tags: [Tag]
}
Receiving a corrupted tag would cause the entire payload to fail when fetching a list of Books.
In the following JSON, note that the tag name is missing from the book My Sweet Swift Book:
[
{
"title": "Dune",
"author": "Frank Herbert",
"tags": [
{
"name": "Sky-Fi"
}
]
},
...
{
"title": "My Sweet Swift Book",
"author": "Me",
"tags": [
{
"name": "Tech"
},
{
}
]
}
]
With the current declaration of Book and Tag, this would cause the entire payload decoding to fail.
In order to allow a class/struct to gain resilience to partial decoding failure, simply add use the @SafeDecoding macro:
@SafeDecoding
struct Book {
...
}
This will implement custom decoding for Book, allowing the single invalid tag to fail, while correctly decoding everything else.
Safe decoding is achieved by attaching a type extension implementing a custom init(from:) (and declaring conformance to Decodable, if necessary).
Within the initializer, safe decoding will be applied to all fitting properties (of type Optional, Array, Set and Dictionary).
@SafeDecoding may be used in any struct or class.
In order to opt-out of safe decoding for a property, simply tag it with @IgnoreSafeDecoding macro.
Let's say an invalid synopsis is enough to invalidate the entire Book:
@SafeDecoding
struct Book {
...
@IgnoreSafeDecoding
let synopsis: String?
...
}
This will cause synopsis to not be safely decoded in the initializer.
The @FallbackDecoding macro can be used to grant fallback semantics to properties.
@FallbackDecoding must be used with @SafeDecoding, and means decoding will never fail properties it is applied to (even if the type is not otherwise "safe-decodable"):
@SafeDecoding
struct Book {
@FallbackDecoding(false)
var isFavourite: Bool
}The RetryDecoding macro can in turn be used to provide alternative decoding of a property; an alternative decoding type and a "mapper" between types must be provided.
An example could be a backend that sometimes returns integers as strings, or booleans as integers:
@SafeDecoding
struct Book {
@RetryDecoding(String.self, map: { $0.lowercased() == "true" })
@RetryDecoding(Int.self, map: { $0 != 0 })
var isFavourite: Bool
}Retries will be performed in the same order as they are declared in the property.
If @FallbackDecoding is used alongside retries, all retries will be attempted before the value specified for fallback is used.
Decoding of enums uses the same @SafeDecoding macro, but requires the decodingStrategy to be specified.
The strategy is itself an enumeration (EnumCaseDecodingStrategy) which holds two cases:
A property name must be specified; this will be itself decoded as a string matching the cases of the enum.
E.g. for the following enum:
@SafeDecoding(decodingStrategy: .caseByObjectProperty("type")
enum MediaAsset {
case vod(...)
case series(...)
case episode(...)
...
}A property type will be decoded, and matched with a case in the MediaAsset type:
{
"type": "vod",
"title": "...",
...
}This will result in a .vod being decoded.
The specified property may or may not be part of the final model.
The standard strategy used when auto-decoding is used, will attempt to decode a root property for each of the enums cases, then decoding the case's parameters.
For the same example specified above, decoding a vod case would require a payload like the one below:
{
"vod": {
"title": "...",
...
}
}The literal name used for an enum case will be the case itself by default, but may be overriden using the @CaseNameDecoding macro:
@SafeDecoding(decodingStrategy: .caseByObjectProperty("type"))
enum MediaAssetKeyed {
@CaseNameDecoding("ASSET/PROGRAMME")
case vod(String?, String)
@CaseNameDecoding("ASSET/SERIES")
case series(id: String, Set<String>)
@CaseNameDecoding("ASSET/EPISODE")
case episode(id: String, title: String, arguments: [String: Double])
}This will try to match cases by decoding a type property, with the property's type itself matching the specified names:
{
"type": "ASSET/EPISODE",
"id": "...
801B
",
...
}
Finally, @SafeDecoding can report errors that occur (and are recovered from) during the decoding process.
This is done by passing the reporter: parameter to the macro:
@SafeDecoding(reporter: ...)
struct Book {
....
}The same applies to enum decoding:
@SafeDecoding(decodingStrategy: ..., reporter: ...)enum MediaAsset { ... }
The reporter must conform the SafeDecodingReporter protocol.
Upon recovery of decoding errors, the reporter will be called with information about said error.
Remember that a reporter is local to its type, i.e. although the same type may be used everywhere each @SafeDecoding usage must be given its reporter expression.
You can use the Swift Package Manager to install your package by adding it as a dependency to your Package.swift file:
dependencies: [
.package(url: "https://github.com/renato-iar/SafeDecoding.git", from: "1.0.0")
]