Codable 中的多种类型

发布于 2025-01-11 23:46:22 字数 705 浏览 0 评论 0原文

我使用的 API 可以根据项目的不同返回一个 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
}

I'm using an API which can either return a Bool or a Int for one value depending on the item. I'm not familiar with how to handle different types for one value in my Codable data. The rated key can either be an object or bool, just unsure how to handle this correctly without getting a typeMismatch error. This is my first time encountering this using an 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
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

故人的歌 2025-01-18 23:46:22

我绝对同意@vadian。您拥有的是可选的评级。在我看来,这是使用 propertyWrapper 的完美场景。这将允许您将此 Rated 类型与任何模型一起使用,而无需为每个模型手动实现自定义编码器/解码器:

@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

{"watchlist":false,"id":550,"favorite":false,"raded":{"value":9}}
{“关注列表”:false,“id”:550,“最喜欢的”:false,“评级”:false}

I definitely agree with @vadian. What you have is an optional rating. IMO this is a perfect scenario for using a propertyWrapper. This would allow you to use this Rated type with any model without having to manually implement a custom encoder/decoder to each model:

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

Usage:

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

This would print:

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

鸩远一方 2025-01-18 23:46:22

我的建议是实现 init(from detector),将 erated 声明为可选的 Double 并解码 Dictionary 或 – if这会失败 - 键 eratedBool 在前一种情况下 erated 设置为 Double 值,在后一种情况下它设置为 < code>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 甚至可以省略。

My suggestion is to implement init(from decoder, declare rated as optional Double and decode a Dictionary or – if this fails – a Bool for key rated. In the former case rated is set to the Double value, in the latter case it's set to 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
        }
    }
}

The line to decode Bool in the else scope can even be omitted.

蓝眼泪 2025-01-18 23:46:22

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

  • 第 1 步 - 为您的 2 种格式创建 2 种类型的结构(来自您的示例,额定 可以是 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 步 的结构
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)
    }
}
  • 最后一步 - 来自 API 的数据 --转换--> 步骤1的结构 --convert--> 第 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)
}

here is an easy way to do it (it's long, but easy to understand)

  • Step 1 - Create 2 types of Struct for your 2 kinds of format (from your sample, rated can be Bool or has Double as value)
// 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?
    }
}
  • Step 2 - Create your AccountState which can hold both kind of format
// 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?
}
  • Step 3 - Make both of your structs of Step 1 able to convert to struct of Step 2
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)
    }
}
  • Final Step - 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)
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文