Best way to map a JSON string to a custom enum case without having access to the enums implementation in Swift?

124 Views Asked by At

Consider the following simple JSON:

{
  "title": "Some title",
  "subtitle": "Some subtitle",
  "button_type": "rounded"
}

This is my current approach towards decoding the buttonType field:

// I dont have access to this enums implementation as it comes from a 3rd party lib.
enum ButtonType {
    case squared
    case simple
    case rounded
    case normal
}

struct ResponseModel: Decodable {
    var title: String
    var subtitle: String
    var type: ButtonType
    
    enum CodingKeys: String, CodingKey {
        case title = "title"
        case subtitle = "subtitle"
        case type = "button_type"
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        title = try container.decode(String.self, forKey: .title)
        subtitle = try container.decode(String.self, forKey: .subtitle)
        let stringType = try container.decode(String.self, forKey: .type)
        switch stringType {
        case "squared":
            type = .squared
        case "simple":
            type = .simple
        case "rounded":
            type = .rounded
        default:
            type = .normal
        }
    }
}

Is there any prettier way to accomplish decoding the string to the custom enum without that nasty switch statement iterating over a plain string?. I sadly do not have access to the enums implementation as it comes from a third party library. If I did I would just make the enum conform to String & Codable and have Codable work its magic, but I dont.

Thanks!

3

There are 3 best solutions below

7
lorem ipsum On BEST ANSWER

You can create your own enum

enum MyButtonType: String {
    case squared
    case simple
    case rounded
    case normal

    var toButtonType: ButtonType {
         switch self {
              case .squared: return .squared
              case .simple: return .simple
              case .rounded: return .rounded
              case .normal: return .normal
         }
    }
}

Then change

 var type: ButtonType

To

 var type: MyButtonType

And when you need the custom enum just use

responseModel.type.toButtonType

The String after the : in the enum makes it conform to RawRepresentable<String> you won’t need the custom init anymore.

You can also manually conform to RawPresentable<String> but it requires all the String` entries.

extension ButtonType: RawRepresentable {
    typealias RawValue = String
    var rawValue: String {
        switch self {
        case .squared:
            return "squared"
        case .simple:
            return "simple"
        case .rounded:
            return "rounded"
        case .normal:
            return "normal"
        }
    }
    
    init?(rawValue: String) {
        switch rawValue {
        case "squared":
            self = .squared
        case "simple":
            self = .simple
        case "rounded":
            self = .rounded
        case "normal":
            self = .normal
        default:
            return nil
        }
    }
}

With this you don't have to change the Type in the model.

1
Luca Angeletti On

Just another solution.

Even if you don't control the how ButtonType is defined, you can still conform it to Decodable using an extension.

Step 1: Conform ButtonType to Decodable

extension ButtonType: Decodable {
  init(from decoder: Decoder) throws {
    let container = try decoder.singleValueContainer()
    let text = try container.decode(String.self)
    switch text {
    case "squared": self = .squared
    case "simple": self = .simple
    case "rounded": self = .rounded
    case "normal": self = .normal
    default: throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Cannot initialize ButtonType from invalid String value \(text)"))
    }
  }
}

Step 2: Use it in your ResponseModel

struct ResponseModel: Decodable {
    var title: String
    var subtitle: String
    var type: ButtonType
    
    enum CodingKeys: String, CodingKey {
        case title = "title"
        case subtitle = "subtitle"
        case type = "button_type"
    }
}
2
Alexander On

There's already two pretty good answers, summarized as:

  1. Create your own enum that mirrors the main one, and serialize it however you want. This has the maintenance burden of keeping your mirror in sync with the main one.
  2. Conform the main enum to Decodable. This is less maintenance work, but it's kinda risky. You should avoid conforming types you don't control (e.g. ButtonType) to protocols you don't control (e.g. Decodable), because it's possible that somebody else has written a conflicting conformance.

There's an alternative trick that I've found to be really handy. You can create a PropertyWrapper, and give it the customized codable behaviour.

@propertyWrapper struct CustomDecodableButtonType {
  let wrappedValue: ButtonType // 1. Define a wrapper for third party type
}

// 2. Define your own decoding logic for it
extension CustomDecodableButtonType: Decodable {
  init(from decoder: Decoder) throws {
    let container = try decoder.singleValueContainer()
    let text = try container.decode(String.self)
    switch text {
      case "squared": self.init(wrappedValue: .squared)
      case "simple" : self.init(wrappedValue: .simple)
      case "rounded": self.init(wrappedValue: .rounded)
      case "normal" : self.init(wrappedValue: .normal)
      default: throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Cannot initialize ButtonType from invalid String value \(text)"))
    }
  }
}

struct ResponseModel: Decodable {
  var title: String
  var subtitle: String
  // 3. Wrap whichever fields you need.
  @CustomDecodableButtonType var type: ButtonType
}