将 JSON 字符串映射到自定义枚举大小写而无需访问 Swift 中的枚举实现的最佳方法?

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

提问人:stompy 提问时间:11/3/2023 最后编辑:HangarRashstompy 更新时间:11/11/2023 访问量:81

问:

请考虑以下简单的 JSON:

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

这是我目前解码buttonType字段的方法:

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

有没有更漂亮的方法可以完成将字符串解码为自定义枚举,而无需那个讨厌的 switch 语句遍历普通字符串?遗憾的是,我无法访问枚举实现,因为它来自第三方库。如果我这样做了,我只会让枚举符合 String & Codable,并让 Codable 发挥它的魔力,但我没有。

谢谢!

JSON Swift 枚举可 编码

评论


答:

2赞 lorem ipsum 11/3/2023 #1

您可以创建自己的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
         }
    }
}

然后更改

 var type: ButtonType

 var type: MyButtonType

当您需要定制时,只需使用enum

responseModel.type.toButtonType

在枚举之后,使其符合您不再需要自定义。String:RawRepresentable<String>init

您也可以手动遵守 String 条目。RawPresentable<String> but it requires all the

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

这样,您就不必更改模型中的“类型”。

评论

0赞 stompy 11/3/2023
这似乎工作正常。谢谢!想知道是否有任何方法可以避免必须使用 switch 语句并且必须“复制”相同的枚举才能将其从一种类型转换为另一种类型。
0赞 lorem ipsum 11/3/2023
尝试“扩展 ButtonType:String {}”,看看它是否有效@stompy
0赞 stompy 11/3/2023
在制作这个线程之前,我确实尝试过哈哈!可悲的是,您收到编译器错误,并显示“继承自非协议类型'字符串'”:(
0赞 lorem ipsum 11/3/2023
@stompy我以为会是这样,但这可能是唯一的办法。您可以建议第三方将其添加到将来的版本中。
0赞 stompy 11/3/2023
恐怕你是对的。无论如何,您的方法比像我一样切换普通字符串要干净得多。您的答案已被标记为已接受。谢谢!:)
1赞 Luca Angeletti 11/3/2023 #2

只是另一种解决方案。

即使您不控制定义方式,您仍然可以使用扩展来使其符合。ButtonTypeDecodable

第 1 步:符合ButtonTypeDecodable

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

第 2 步:在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"
    }
}

评论

0赞 stompy 11/13/2023
我想到了这个,是的!但是,如果我团队中的其他人也想用 Decodable 扩展这个第三方枚举(也许是另一种解码策略或其他什么),这两个扩展都会发生冲突。谢谢!
1赞 Alexander 11/10/2023 #3

已经有两个非常好的答案,总结为:

  1. 创建自己的枚举来镜像主枚举,并根据需要对其进行序列化。这有保持镜子与主镜子同步的维护负担。
  2. 使主枚举符合 。这减少了维护工作,但有点风险。您应该避免将您无法控制的类型(例如)与您无法控制的协议(例如)保持一致,因为其他人可能已经编写了冲突的一致性。DecodableButtonTypeDecodable

我发现还有另一种技巧非常方便。您可以创建一个 PropertyWrapper,并为其提供自定义的可编码行为。

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

评论

0赞 stompy 11/13/2023
哦,哇。我从未深入研究过涉及属性包装器的解决方案。这非常方便。谢谢!
0赞 Alexander 11/13/2023
@stompy是的,这是一个非常强大的功能。好吧,它本身并不是一个真正的功能,而是另外两个功能(属性包装器和 Codable)之间非常自然的相互作用。为外来类型提供可编码的实现是它真正有用的地方之一。另一个有用的情况是,如果你有一种类型,它以不同的方式编码用于不同的用途(例如,两个不同的 API 想要相同的数据,但有两种不同的格式)。