Kodable
is an extension of the Codable
functionality through property wrappers. The main goal is to remove boilerplate while also adding useful functionality.
Features:
- No need to write your own
init(from decoder: Decoder)
orCodingKeys
- Provide a custom key for decoding
- Access nested values using the
.
notation - Add a default value in case the value is missing
- Overriding the values decoded (i.e. trimming a string)
- Validation of the values decoded
- Automatically tries to decode
String
andBool
from other types as a fallback - Transformer protocol to implement your own additional functionality on top of the existing ones
If you're working directly in a Package, add Kodable to your Package.swift file
dependencies: [
.package(url: "https://github.com/JARMourato/Kodable.git", .upToNextMajor(from: "1.0.0" )),
]
If working in an Xcode project select File->Swift Packages->Add Package Dependency...
and search for the package name: Kodable
or the git url:
https://github.com/JARMourato/Kodable.git
Just make your type conform to Kodable
and you'll have access to all of the features Coding
brings.
You can mix and match Codable
values with Coding
properties.
Declare your model:
struct User: Kodable {
var identifier: String = ""
var social: String?
@Coding("first_name") var firstName: String
@Coding(default: "+1 123456789") var phone: String
@Coding("address.zipCode") var zipCode: Int
}
// Instead of
struct CodableUser: Codable {
enum Keys: String, CodingKey {
case identifier, social, firstName = "first_name", phone, address
}
enum NestedKeys: String, CodingKey {
case zipCode
}
var identifier: String = ""
var social: String?
var firstName: String
var phone: String
var zipCode: Int
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: Keys.self)
identifier = try container.decode(String.self, forKey: .identifier)
social = try container.decodeIfPresent(String.self, forKey: .social)
firstName = try container.decode(String.self, forKey: .firstName)
phone = try container.decodeIfPresent(String.self, forKey: .phone) ?? "+1 123456789"
let addressContainer = try container.nestedContainer(keyedBy: NestedKeys.self, forKey: .address)
zipCode = try addressContainer.decode(Int.self, forKey: .zip)
}
}
Then
let json = """
{
identifier: "1",
"social": 987654321,
"first_name": John,
"address": {
"zipCode": 94040,
}
}
""".data(using: .utf8)!
let result = try JSONDecoder().decode(User.self, from: json)
// or using the provided syntactic sugar
let user = try User.decode(from: json)
This wrapper allows decoding dates on per-property strategy basis. By default, CodableDate
uses the iso8601
strategy. The built-in strategies are:
iso8601, rfc2822, rfc3339 and timestamp
. There is also the option of using a custom format by providing a valid string format to the option .format()
.
struct Dates: Kodable {
@CodableDate var iso8601: Date
@CodableDate(.format("y-MM-dd"), .key("simple_date")) var simpleDate: Date
@CodableDate(.rfc2822, .key("rfc2822")) var rfc2822Date: Date
@CodableDate(.rfc3339, .key("rfc3339")) var rfc3339Date: Date
@CodableDate(.timestamp, .key("timestamp")) var timestamp: Date
}
let json = """
{
"iso8601": "1996-12-19T16:39:57-08:00",
"simple_date": "2001-01-01",
"rfc2822": "Thu, 19 Dec 1996 16:39:57 GMT",
"rfc3339": "1996-12-19T16:39:57-08:00",
"timestamp": 978307200.0,
}
""".data(using: .utf8)!
let dates = Dates.decode(from: json)
print(dates.iso8601.description) // Prints "1996-12-20 00:39:57 +0000"
print(dates.simpleDate.description) // Prints "2001-01-01 00:00:00 +0000"
print(dates.rfc2822Date.description) // Prints "1996-12-19 16:39:57 +0000"
print(dates.rfc3339Date.description) // Prints "1996-12-20 00:39:57 +0000"
print(dates.timestamp.description) // Prints "2001-01-01 00:00:00 +0000"
For the types Array
, Bool
, and String
, some lossy decoding was introduced. More types can be added later on, but for now these sufficed my personal usage. To disable this behavior for a specific property, in case you want decoding to fail when the type is not correct, just provide the enforceTypeDecoding
option to the Coding
property wrapper.
The lossy decoding on Array
is done by trying to decode each element from a Array.Element
type in a non-lossy way (even if they are Bool
or String
) and ignores values that fail decoding.
struct LossyArray: Kodable {
@Coding("failable_array") var array: [String]
}
let json = """
{
"failable_array": [ "1", 1.5, "2", true, "3", null, 4 ]
}
""".data(using: .utf8)!
let lossy = try LossyArray.decode(from: json)
print(lossy.array) // Prints [ "1", "2", "3" ]
Tries to decode a Bool
from Int
or String
if Bool
fails
struct Fail: Kodable {
@Coding("string_bool", .enforceTypeDecoding) var notBool: Bool
}
struct Success: Kodable {
@Coding("string_bool") var stringBool: Bool
@Coding("int_bool") var intBool: Bool
}
let json = """
{
"string_bool": "false",
"int_bool": 1,
}
""".data(using: .utf8)!
let success = try Success.decode(from: json)
print(success.stringBool) // prints false
print(success.intBool) // prints true
let fail = try Fail.decode(from: json) // Throws KodableError.invalidValueForPropertyWithKey("string_bool")
Tries to decode a String
from Double
or Int
if String
fails
struct Amounts: Kodable {
@Coding("double") var double: String
@Coding("int") var integer: String
@Coding var string: String
}
let json = """
{
"double": 629.9,
"int": 1563,
"string": "999.9"
}
""".data(using: .utf8)!
let amounts = try Amounts.decode(from: json)
print(amounts.double) // prints "629.9"
print(amounts.integer) // prints "1563"
print(amounts.string) // prints "999.9"
You can provide a KodableModifier.custom
modifier with an overriding closure so that you can modify the decoded value before assigning it to the property.
struct Project: Kodable {
@Coding(Project.trimmed) var title: String
static var trimmed: KodableModifier<String> {
KodableModifier.custom { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
}
}
let json = #"{ "title": " A New Project " }"#.data(using: .utf8)!
let project = try Project.decode(from: json)
print(project.title) // Prints "A New Project"
There are a few built in modifiers provided already:
String
trimmed
: AppliestrimmingCharacters(in: .whitespacesAndNewlines)
to the value decoded
String?
trimmed
: AppliestrimmingCharacters(in: .whitespacesAndNewlines)
to the value decodedtrimmedNifIfEmpty
: AppliestrimmingCharacters(in: .whitespacesAndNewlines)
to the value decoded, returns nif if empty
Comparable
clamping(to:)
: Clamps the value in a range.range()
: Constrains the value inside a provided range.max()
: Constrains the value to a maximum value.min()
: Constrains the value to a minimum value.
You can provide a KodableModifier.validation
modifier with a validation closure, where you can verify if the value is valid.
struct Image: Kodable {
@Coding(.validation({ $0 > 500 })) var width: Int
}
let json = #{ "width": 400 }#.data(using: .utf8)!
let image = try Image.decode(from: json)
// Throws KodableError.validationFailed(property: "width", parsedValue: 400)
Kodable
was built based on a protocol called KodableTransform
public protocol KodableTransform {
associatedtype From: Codable
associatedtype To
func transformFromJSON(value: From) throws -> To
func transformToJSON(value: To) throws -> From
init()
}
If you want to add your own custom behavior, you can create a type that conforms to the KodableTransform
protocol.:
struct URLTransformer: KodableTransform {
enum Error: Swift.Error {
case failedToCreateURL
}
func transformFromJSON(value: String) throws -> URL {
guard let url = URL(string: value) else { throw Error.failedToCreateURL }
return url
}
func transformToJSON(value: URL) throws -> String {
value.absoluteString
}
}
Then use the KodableTrasformable
property wrapper, upon which all other wrappers are based:
typealias CodingURL = KodableTransformable<URLTransformer>
And voilà
struct Test: Kodable {
@CodingURL("html_url") var url: URL
}
If you feel like something is missing or you want to add any new functionality, please open an issue requesting it and/or submit a pull request with passing tests 🙌
MIT
João (@_JARMourato)