Codable 中的多种类型

multiple types in Codable

我正在使用 API,它可以是 return、BoolInt 的一个值,具体取决于项目。我不熟悉如何处理 Codable 数据中一个值的不同类型。额定键可以是对象或布尔值,只是不确定如何正确处理它而不会出现 typeMismatch 错误。这是我第一次使用 API.

遇到这种情况
{"id":550,"favorite":false,"rated":{"value":9.0},"watchlist":false}
{“id":405,"favorite":false,"rated":false,"watchlist":false}
struct AccountState: Codable {
    let id: Int?
    let favorite: Bool?
    let watchlist: Bool?
    let rated: Rated?
}

struct Rated : Codable {
    let value : Int? // <-- Bool or Int
}

我的建议是实现 init(from decoder,将 rated 声明为可选 Double 并解码 Dictionary 或 - 如果失败 - Boolrated。在前一种情况下 rated 设置为 Double 值,在后一种情况下它设置为 nil.

struct AccountState: Decodable {
    let id: Int
    let favorite: Bool
    let watchlist: Bool
    let rated: Double?
     
    private enum CodingKeys: String, CodingKey { case id, favorite, watchlist, rated }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(Int.self, forKey: .id)
        favorite = try container.decode(Bool.self, forKey: .favorite)
        watchlist = try container.decode(Bool.self, forKey: .watchlist)
        if let ratedData = try? container.decode([String:Double].self, forKey: .rated),
           let key = ratedData.keys.first, key == "value"{
            rated = ratedData[key]
        } else {
            let _ = try container.decode(Bool.self, forKey: .rated)
            rated = nil
        }
    }
}

else 范围内解码 Bool 的行甚至可以省略。

这是一个简单的方法(它很长,但很容易理解)

  • 第 1 步 - 为您的 2 种格式创建 2 种类型的结构(根据您的示例,rated 可以是 Bool 或具有 Double 作为值)
// Data will convert to this when rate is bool
struct AccountStateRaw1: Codable {
    let id: Int?
    let favorite: Bool?
    let watchlist: Bool?
    let rated: Bool?
}

// Data will convert to this when rate has value
struct AccountStateRaw2: Codable {
    let id: Int?
    let favorite: Bool?
    let watchlist: Bool?
    let rated: Rated?

    struct Rated : Codable {
        let value : Double?
    }
}
  • 第 2 步 - 创建可以容纳两种格式的 AccountState
// You will use this in your app, and you need the data you get from API to convert to this
struct AccountState: Codable {
    let id: Int?
    let favorite: Bool?
    let watchlist: Bool?
    let ratedValue: Double?
    let isRated: Bool?
}
  • 第 3 步 - 使 第 1 步 的两个结构都能够转换为 第 2 步[的结构] =37=]
protocol AccountStateConvertable {
    var toAccountState: AccountState { get }
}

extension AccountStateRaw1: AccountStateConvertable {
    var toAccountState: AccountState {
        AccountState(id: id, favorite: favorite, watchlist: watchlist, ratedValue: nil, isRated: rated)
    }
}

extension AccountStateRaw2: AccountStateConvertable {
    var toAccountState: AccountState {
        AccountState(id: id, favorite: favorite, watchlist: watchlist, ratedValue: rated?.value, isRated: nil)
    }
}
  • 最后一步 - data from API --convert--> structs of Steps 1 --convert--> struct of Step 2
func convert(data: Data) -> AccountState? {
    func decodeWith<T: Codable & AccountStateConvertable>(type: T.Type) -> AccountState? {
        let parsed: T? = try? JSONDecoder().decode(T.self, from: data)
        return parsed?.toAccountState
    }
    return decodeWith(type: AccountStateRaw1.self) ?? decodeWith(type: AccountStateRaw2.self)
}

我完全同意@vadian。您拥有的是可选评级。 IMO 这是使用 propertyWrapper 的完美方案。这将允许您将此 Rated 类型与任何模型一起使用,而无需手动为每个模型实现自定义 encoder/decoder:

@propertyWrapper
struct RatedDouble: Codable {

    var wrappedValue: Double?
    init(wrappedValue: Double?) {
        self.wrappedValue = wrappedValue
    }

    private struct Rated: Decodable {
        let value: Double
    }

    public init(from decoder: Decoder) throws {
        do {
            wrappedValue = try decoder.singleValueContainer().decode(Rated.self).value
        } catch DecodingError.typeMismatch {
            let bool = try decoder.singleValueContainer().decode(Bool.self)
            guard !bool else {
                throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Corrupted data"))
            }
            wrappedValue = nil
        }
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        guard let double = wrappedValue else {
            try container.encode(false)
            return
        }
        try container.encode(["value": double])
    }
}

用法:

struct AccountState: Codable {
    let id: Int?
    let favorite: Bool?
    let watchlist: Bool?
    @RatedDouble var rated: Double?
}

let json1 = #"{"id":550,"favorite":false,"rated":{"value":9.0},"watchlist":false}"#
let json2 = #"{"id":550,"favorite":false,"rated":false,"watchlist":false}"#
do {
    let accountState1 = try JSONDecoder().decode(AccountState.self, from: Data(json1.utf8))
    print(accountState1.rated ?? "nil")  // "9.0\n"
    let accountState2 = try JSONDecoder().decode(AccountState.self, from: Data(json2.utf8))
    print(accountState2.rated ?? "nil")  // "nil\n"
    let encoded1 = try JSONEncoder().encode(accountState1)
    print(String(data: encoded1, encoding: .utf8) ?? "nil")
    let encoded2 = try JSONEncoder().encode(accountState2)
    print(String(data: encoded2, encoding: .utf8) ?? "nil")
} catch {
    print(error)
}

这将打印:

9.0
nil
{"watchlist":false,"id":550,"favorite":false,"rated":{"value":9}}
{"watchlist":false,"id":550,"favorite":false,"rated":false}