Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift Codable - How to Initialize an Optional Enum Property in a Failable Manner

I'm trying to adopt the Codable protocol for an object that must be instantiated from a JSON my web service returns in response to one of the API calls.

One of the properties is of enumeration type, and optional: nil means that none of the options defined by the enum has been chosen.

The enum constants are Int-based and start at 1, not 0:

class MyClass: Codable {

    enum Company: Int {
        case toyota = 1
        case ford
        case gm
    } 
    var company: Company?

This is because the value 0 on the corresponding JSON entry is reserved for "not set"; i.e. it should be mapped to nil when setting initializing the company property from it.

Swift's enum initializer init?(rawValue:) provides this functionality out-of-the-box: an Int argument that does not match the raw value of any case will cause the initializer to fail and return nil. Also, Int (and String) -based enums can be made to conform to Codable just by declaring it in the type definition:

enum Company: Int, Codable {
    case toyota = 1
    case ford
    case gm
} 

The problem is, my custom class has more than 20 properties, so I really really want to avoid having to implement init(from:) and encode(to:), relying instead on the automatic behavior obtained by providing the CondingKeys custom enumeration type.

This results in initialization of the whole class instance failing because it seems the "synthesized" initializer cannot infer that an unsupported raw value of the enum type should be treated as nil (even though the target property is clearly marked as optional, i.e. Company?).

I think this is so because the initializer provided by Decodable can throw, but it can not return nil:

// This is what we have:
init(from decoder: Decoder) throws

// This is what I would want:
init?(from decoder: Decoder)

As a workaround, I have implemented my class as follows: map the JSON's integer property into a private, stored Int property of my class that serves as storage only, and introduce a strongly-typed computed property that acts as a bridge between the storage and the rest of my app:

class MyClass {

   // (enum definition skipped, see above)

   private var companyRawValue: Int = 0

   public var company: Company? {
       set {
           self.companyRawValue = newValue?.rawValue ?? 0
           // (sets to 0 if passed nil)
       }
       get {
           return Company(rawValue: companyRawValue)
           // (returns nil if raw value is 0)
       }
   }

   enum CodingKeys: String, CodingKey {
       case companyRawValue = "company"
   }

   // etc...

My question is: Is there a better (simpler/more elegant) way, that:

  1. Does not require duplicated properties like my workaround, and
  2. Does not require fully implementing init(from:) and/or encode(with:), perhaps implementing simplified versions of these that delegate to the default behavior for the most part (i.e. do not require the whole boilerplate of manually initializing/encoding each and every property)?

Addendum: There's a third, also inelegant solution that didn't occur to me when I first posted the question. It involves using an artificial base class just for the sake of automatic decoding. I will not use it, but just describe it here for the sake of completeness:

// Groups all straight-forward decodable properties
//
class BaseClass: Codable {
    /*
     (Properties go here)
     */

    enum CodingKeys: String, CodingKey {
        /*
         (Coding keys for above properties go here)
         */
    }

    // (init(from decoder: Decoder) and encode(to:) are 
    // automatically provided by Swift)
}

// Actually used by the app
//
class MyClass: BaseClass {

    enum CodingKeys: String, CodingKey {
        case company
    }

    var company: Company? = nil

    override init(from decoder: Decoder) throws {
        super.init(from: decoder)

        let values = try decoder.container(keyedBy: CodingKeys.self)
        if let company = try? values.decode(Company.self, forKey: .company) {
            self.company = company
        }

    }
}

...But this is a really ugly hack. Class inheritance hierarchy shouldn't be dictated by this type of shortcomings.

like image 476
Nicolas Miari Avatar asked Nov 06 '18 03:11

Nicolas Miari


2 Answers

I think I had a similar issue to yours, if I'm understanding correctly. In my case, I wrote a wrapper for each enum in question:

struct NilOnFail<T>: Decodable where T: Decodable {

    let value: T?

    init(from decoder: Decoder) throws {
        self.value = try? T(from: decoder) // Fail silently
    }

    // TODO: implement Encodable
}

Then use it like this:

class MyClass: Codable {

    enum Company: Int {
        case toyota = 1
        case ford
        case gm
    } 

    var company: NilOnFail<Company>
...

The caveat being, of course, that wherever you need to access the value of company you need to use myClassInstance.company.value

like image 84
Max Chuquimia Avatar answered Nov 10 '22 11:11

Max Chuquimia


From swift 5 you can use Property Wrappers. https://docs.swift.org/swift-book/LanguageGuide/Properties.html

In your case main struct will be like:

@propertyWrapper
public struct NilOnFailCodable<ValueType>: Codable where ValueType: Codable {

    public var wrappedValue: ValueType?

    public init(wrappedValue: ValueType?) {
        self.wrappedValue = wrappedValue
    }

    public init(from decoder: Decoder) throws {
        self.wrappedValue = try? ValueType(from: decoder)
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        if let value = wrappedValue {
            try container.encode(value)
        } else {
            try container.encodeNil()
        }
    }
}

Usage

struct Model: Codable {
    @NilOnFailCodable var val: Enum?
    enum Enum: Int, Codable {
        case holdUp = 0
        case holdDown = 1
    }
}

And example

let encoder = JSONEncoder()
let decoder = JSONDecoder()
let s = #"{"val": 2}"#
let data = s.data(using: .utf8)
let dec = decoder.decode(Model.self, from: data!)
print(dec)
let enc = encoder.encode(dec)
print(decoder.decode(Model.self, from: enc))

Will print

Model(_val: NilOnFailCodable<Model.Enum>(wrappedValue: nil))
nil
Model(_val: NilOnFailCodable<Model.Enum>(wrappedValue: nil))
nil

And for value "val": 1

Model(_val: NilOnFailCodable<Model.Enum>(wrappedValue: Optional(Model.Enum.holdDown)))
Optional(1)
Model(_val: NilOnFailCodable<Model.Enum>(wrappedValue: Optional(Model.Enum.holdDown)))
Optional(1)
like image 23
Willian Avatar answered Nov 10 '22 09:11

Willian