Home > Mobile >  multiple types in Codable
multiple types in Codable

Time:03-07

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