struct ModelLevel2: CodableEntity, DefaultConstructible {
@CodableScalar
var intProperty: Int = 0
@CodableScalar(key: "Ratio", mandatory: true)
var floatProperty: Float = 1.0
@CodableScalar(mandatory: true)
var name: String
@CodableUIColor
var color: UIColor = .black
@CodableURL(key: "ugly_namedURL")
var link: URL = URL(string: "file://")!
static var codableKeyPaths = KeyPathList{
\Self._intProperty
\Self._floatProperty
\Self._name
\Self._color
\Self._link
}
}
struct ModelLevel1: CodableEntity, DefaultConstructibel {
@CodableProperty
var details: ModelLevel2
@CodableISO8601Date
var date
static var codableKeyPaths = KeyPathList{
\Self._details
\Self._date
}
}
struct ModelRoot: CodableEntity, DefaultConstructible {
@CodableArrayProperty
var items: [ModelLevel1]
static var codableKeyPaths = KeyPathList{
\Self._items
}
}
Usually Codable is used to deserialize JSON to the object. But in some cases it is required to implement complex custom init(from decoder: Decoder). Codable property utility solves next problems:
- Codable struct contains either autogenerated or custom defined enum CodingKeys. Default implementation demand every name defined in CodingKeys must exist in JSON. If you want to implement “stable” strategy you have to use method decodeIfPresent(_: forKey:). If you have to deal with response where JSON contains only part of mandatory keys or for some reason response depends on some mystery events from another galaxy and backend developer is inaccessible or thinks it should be so you have a problem. Codable property implements the next strategy: it collects the information about declared properties and then tries to find each of them in JSON response using mapping or actual property name. It rises an exception only in case when property was marked as mandatory and was not found. As a result init(from decoder: Decoder) constructs result struct even using empty JSON and you can check if the property has default value or not.
- Sometimes for some unknown reason backend developer uses big case for keys alongside with small case in the same response. As a result you receive keys: ItemName, ITEMNAME, itemName for same response. Or after some changes on the backend side you receive something like “Attributeextension” instead of previously agreed “attributeExtension”. Don’t ask me why - I just had to deal with it. Just ended it up with using case insensitive comparison for keys.
- Very common error when you receive integer instead of float, integer instead of bool, string instead of integer and vice versa. Codable property tries to fix typeMismatch errors. Every property use Traits (or Strategy) type which contains static fallback decoding method. There are set of predefined traits. Utility can convert “yes/no” or “TRUE/FALSE” to Bool. If fallback method fails typeMismatch will be raised. Same logic for optionals.
- When default implementation parses arrays it fails when even one element is invalid by some reason. Stable array strategy produces result array with only valid elements and omit all errors.
- Nil value is skipped for non-optionals properties. Optional will be reset to nil. If mandatory property contains optional value (weird but it’s working) it will be reset. But if key doesn’t exist for mandatory property the exception keyNotFound will be raised.
- Default implementation uses strategy for date properties. You can define custom strategy. But this strategy will be applied to every property that has type date. If JSON contains dates with different formats you have to implement decoding for each of them. Codable property can use different date strategy for different keys. It can deal with internet dates ISO8601.
- Sometimes you need flattened struct. You can provide the path for property like level1property.level2property….levelNproperty. Decoding will be the same like flatMap converts dictionary of dictionaries to flat dictionary.
- CodableProperty may dealing with dynamic keys. Backend produces the dictionary with some arbitrary keys and every key contains object. Decoding produces the array of object. This JSON can be decoded as Array.
{
"dynamicS001": {
"name": "test1",
"param": 1
},
"dynamicS002": {
"name": "test2",
"param": 10
},
"dynamicS003": {
"name": "testN",
"param": 0
}
}
For Integers, Strings, Floats, Bools:
@CodableScalar
var propertyName: Int = 10
Provide mandatory flag if needed:
@CodableScalar(mandatory: true)
var stringProperty: String
Provide key name mapping if needed:
@CodableScalar(key: "mappedName-in-JSON")
var boolProperty = false
Provide decoding path if needed:
@CodableScalar(key: “level1.level2.keyName-to-be-flattened”)
var boolProperty = false
For objects, codable enums:
@CodableProperty var item = ItemType()
For optionals:
@CodableOptionalScalar var intProperty: Int? = nil
For stable arrays:
@CodableArrayProperty
var arrayProperty: [ItemType]
For array to flatten:
@CodableFlattenedArrayProperty
var items: [FlattenTestItem]
For stable bool decoding:
@CodableStableBool
var boolProperty1 = false
For ISO 8601 date decoding:
@CodableISO8601Date
var date: Date
For UIColor decoding next format is used - '#RRGGBB'
@CodableUIColor
var color: UIColor
For URL decoding:
@CodableURL
var url: URL = URL(string: "file://")!
Wrapping codable property into wrapper is only half of the way. Target struct must conform CodableEntity and DefaultConstructible protocols. And the most important - struct must contain declaration of static property codableKeyPaths. Library provides result builder KeyPathList for that. So the declaration will be:
struct Model: CodableEntity, DefaultConstructible {
@CodableScalar(mandatory: true)
var name: String
@CodableISO8601Date
var date: Date
@CodableUIColor
var color: UIColor
@CodableURL(key: "url")
var link: URL = URL(string: “file://")!
static var codableKeyPaths = KeyPathList{
\Self._date
\Self._color
\Self._link
\Self._name
}
}
The corresponding JSON:
{
"name" "test"
"date": "2022-01-31T02:22:40Z",
"color": "#AF4E10",
"url": "https://test.com/path"
}
Pay attention: if you declare property using property wrapper and don’t include its keypath to codableKeyPaths list it will not be decoded.
You can completely customize decoding process implementing your own traits. Default implementation exists for all parts.
public protocol CodableTraits
{
associatedtype StorableType
associatedtype CodableType: Codable
typealias DecodeContainer = KeyedDecodingContainer<DynamicCodingKey>
static var pathSeparator: String { get }
static func findCodingKey(name: String, container: DecodeContainer) -> DynamicCodingKey?
//key exists for sure
static func decode(from container: DecodeContainer, _ dynamicKey: DynamicCodingKey) throws -> CodableType
//key exists for sure
static func assignStorableNil(from container: DecodeContainer,
dynamicKey: DynamicCodingKey,
mandatory: Bool,
value: inout StorableType) -> Bool
//key exists for sure
static func fallback(from container: DecodeContainer, _ dynamicKey: DynamicCodingKey) -> CodableType?
static func createStorable(_ codable: CodableType) -> StorableType?
static func createDecodable(_ storable: StorableType) -> CodableType?
}
StorableType - type for value to be stored in wrapper and represented.
CodableType - type for value that actually was read from JSON. As an example UIColor property read its value as String.
pathSeparator - you can define your own separator instead of default “.”.
findCodingKey - implement your algorithm for searching key in the container.
decode - implement you custom decoding here.
assignStorableNill - handler for json’s null values.
fallback - if you need some robustness implement you first aid algorithm.
createStorable - convert CodableType value to StorableType.
createDecodable - convert StorableType value to DecodableType. It’s for encode process.
As an example let’s handle array of strings:
{
"value": [ "string1", "string2", "strings3" ]
}
struct CustomCodableTraits: CodableTraits {
static func createDecodable(_ storable: String) -> [String]? {
storable.components(separatedBy: ",")
}
static func createStorable(_ codable: [String]) -> String? {
codable.joined(separator: ",")
}
}
The property will be declared as:
@CodableKeyedProperty<CustomCodableTraits>
var value
And it will contain value "string1,string2,string3" after decoding.
CodableProperty was implemented by Stan Reznichenko to pretect you from evil backend developers.