How to use Any in Codable Type

后端 未结 11 645
猫巷女王i
猫巷女王i 2020-11-29 04:20

I\'m currently working with Codable types in my project and facing an issue.

struct Person: Codable
{
    var id: Any
}

相关标签:
11条回答
  • 2020-11-29 05:20

    Here your id can be any Codable type:

    Swift 4.2

    struct Person<T: Codable>: Codable {
    
        var id: T
        var name: String?
    }
    
    let p1 = Person(id: 1, name: "Bill")
    let p2 = Person(id: "one", name: "John")
    
    0 讨论(0)
  • 2020-11-29 05:21

    Simply you can use AnyCodable type from Matt Thompson's cool library AnyCodable.

    Eg:

    import AnyCodable
    
    struct Person: Codable
    {
        var id: AnyCodable
    }
    
    0 讨论(0)
  • 2020-11-29 05:21

    To make key as Any, I like all above answers. But when you are not sure which data type your server guy will send then you use Quantum class (as above), But Quantum type is little difficult to use or manage. So here is my solution to make your decodable class key as a Any data type (or "id" for obj-c lovers)

       class StatusResp:Decodable{
        var success:Id? // Here i am not sure which datatype my server guy will send
    }
    enum Id: Decodable {
    
        case int(Int), double(Double), string(String) // Add more cases if you want
    
        init(from decoder: Decoder) throws {
    
            //Check each case
            if let dbl = try? decoder.singleValueContainer().decode(Double.self),dbl.truncatingRemainder(dividingBy: 1) != 0  { // It is double not a int value
                self = .double(dbl)
                return
            }
    
            if let int = try? decoder.singleValueContainer().decode(Int.self) {
                self = .int(int)
                return
            }
            if let string = try? decoder.singleValueContainer().decode(String.self) {
                self = .string(string)
                return
            }
            throw IdError.missingValue
        }
    
        enum IdError:Error { // If no case matched
            case missingValue
        }
    
        var any:Any{
            get{
                switch self {
                case .double(let value):
                    return value
                case .int(let value):
                    return value
                case .string(let value):
                    return value
                }
            }
        }
    }
    

    Usage :

    let json = "{\"success\":\"hii\"}".data(using: .utf8) // response will be String
            //let json = "{\"success\":50.55}".data(using: .utf8)  //response will be Double
            //let json = "{\"success\":50}".data(using: .utf8) //response will be Int
            let decoded = try? JSONDecoder().decode(StatusResp.self, from: json!)
            print(decoded?.success) // It will print Any
    
            if let doubleValue = decoded?.success as? Double {
    
            }else if let doubleValue = decoded?.success as? Int {
    
            }else if let doubleValue = decoded?.success as? String {
    
            }
    
    0 讨论(0)
  • 2020-11-29 05:23

    First of all, as you can read in other answers and comments, using Any for this is not good design. If possible, give it a second thought.

    That said, if you want to stick to it for your own reasons, you should write your own encoding/decoding and adopt some kind of convention in the serialized JSON.

    The code below implements it by encoding id always as string and decoding to Int or String depending on the found value.

    import Foundation
    
    struct Person: Codable {
        var id: Any
    
        init(id: Any) {
            self.id = id
        }
    
        public init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: Keys.self)
            if let idstr = try container.decodeIfPresent(String.self, forKey: .id) {
                if let idnum = Int(idstr) {
                    id = idnum
                }
                else {
                    id = idstr
                }
                return
            }
            fatalError()
        }
    
        func encode(to encoder: Encoder) throws {
            var container = encoder.container(keyedBy: Keys.self)
            try container.encode(String(describing: id), forKey: .id)
        }
    
        enum Keys: String, CodingKey {
            case id
        }
    }
    
    extension Person: CustomStringConvertible {
        var description: String { return "<Person id:\(id)>" }
    }
    

    Examples

    Encode object with numeric id:

    var p1 = Person(id: 1)
    print(String(data: try JSONEncoder().encode(p1), 
          encoding: String.Encoding.utf8) ?? "/* ERROR */")
    // {"id":"1"}
    

    Encode object with string id:

    var p2 = Person(id: "root")
    print(String(data: try JSONEncoder().encode(p2), 
          encoding: String.Encoding.utf8) ?? "/* ERROR */")
    // {"id":"root"}
    

    Decode to numeric id:

    print(try JSONDecoder().decode(Person.self, 
          from: "{\"id\": \"2\"}".data(using: String.Encoding.utf8)!))
    // <Person id:2>
    

    Decode to string id:

    print(try JSONDecoder().decode(Person.self, 
          from: "{\"id\": \"admin\"}".data(using: String.Encoding.utf8)!))
    // <Person id:admin>
    

    An alternative implementation would be encoding to Int or String and wrap the decoding attempts in a do...catch.

    In the encoding part:

        if let idstr = id as? String {
            try container.encode(idstr, forKey: .id)
        }
        else if let idnum = id as? Int {
            try container.encode(idnum, forKey: .id)
        }
    

    And then decode to the right type in multiple attempts:

    do {
        if let idstr = try container.decodeIfPresent(String.self, forKey: .id) {
            id = idstr
            id_decoded = true
        }
    }
    catch {
        /* pass */
    }
    
    if !id_decoded {
        do {
            if let idnum = try container.decodeIfPresent(Int.self, forKey: .id) {
                id = idnum
            }
        }
        catch {
            /* pass */
        }
    }
    

    It's uglier in my opinion.

    Depending on the control you have over the server serialization you can use either of them or write something else adapted to the actual serialization.

    0 讨论(0)
  • 2020-11-29 05:24

    There is a corner case which is not covered by Luca Angeletti's solution.

    For instance, if Cordinate's type is Double or [Double], Angeletti's solution will cause an error: "Expected to decode Double but found an array instead"

    In this case, you have to use nested enum instead in Cordinate.

    enum Cordinate: Decodable {
        case double(Double), array([Cordinate])
    
        init(from decoder: Decoder) throws {
            if let double = try? decoder.singleValueContainer().decode(Double.self) {
                self = .double(double)
                return
            }
    
            if let array = try? decoder.singleValueContainer().decode([Cordinate].self) {
                self = .array(array)
                return
            }
    
            throw CordinateError.missingValue
        }
    
        enum CordinateError: Error {
            case missingValue
        }
    }
    
    struct Geometry : Decodable {
        let date : String?
        let type : String?
        let coordinates : [Cordinate]?
    
        enum CodingKeys: String, CodingKey {
    
            case date = "date"
            case type = "type"
            case coordinates = "coordinates"
        }
    
        init(from decoder: Decoder) throws {
            let values = try decoder.container(keyedBy: CodingKeys.self)
            date = try values.decodeIfPresent(String.self, forKey: .date)
            type = try values.decodeIfPresent(String.self, forKey: .type)
            coordinates = try values.decodeIfPresent([Cordinate].self, forKey: .coordinates)
        }
    }
    
    0 讨论(0)
提交回复
热议问题