Codable 中的多种类型
multiple types in Codable
我正在使用 API,它可以是 return、Bool
或 Int
的一个值,具体取决于项目。我不熟悉如何处理 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
或 - 如果失败 - Bool
键 rated
。在前一种情况下 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}
我正在使用 API,它可以是 return、Bool
或 Int
的一个值,具体取决于项目。我不熟悉如何处理 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
或 - 如果失败 - Bool
键 rated
。在前一种情况下 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}