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
}
CodePudding user response:
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.
CodePudding user response:
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?
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}
CodePudding user response:
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 beBool
or hasDouble
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)
}