Skip to content

Generic property wrapper to extend Codable protocol with automatic handling of common errors during JSON parsing.

License

Notifications You must be signed in to change notification settings

singularstan/CodableProperty

Repository files navigation

CodableProperty

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
  }
}

Reason to use

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. 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
  }
}

How to use CodableProperty

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.

How to implement custom traits.

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.

Credits

CodableProperty was implemented by Stan Reznichenko to pretect you from evil backend developers.

About

Generic property wrapper to extend Codable protocol with automatic handling of common errors during JSON parsing.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages