How to use Any in Codable Type

后端 未结 11 644
猫巷女王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:02

    I solved this issue defining a new Decodable Struct called AnyDecodable, so instead of Any I use AnyDecodable. It works perfectly also with nested types.

    Try this in a playground:

    var json = """
    {
      "id": 12345,
      "name": "Giuseppe",
      "last_name": "Lanza",
      "age": 31,
      "happy": true,
      "rate": 1.5,
      "classes": ["maths", "phisics"],
      "dogs": [
        {
          "name": "Gala",
          "age": 1
        }, {
          "name": "Aria",
          "age": 3
        }
      ]
    }
    """
    
    public struct AnyDecodable: Decodable {
      public var value: Any
    
      private struct CodingKeys: CodingKey {
        var stringValue: String
        var intValue: Int?
        init?(intValue: Int) {
          self.stringValue = "\(intValue)"
          self.intValue = intValue
        }
        init?(stringValue: String) { self.stringValue = stringValue }
      }
    
      public init(from decoder: Decoder) throws {
        if let container = try? decoder.container(keyedBy: CodingKeys.self) {
          var result = [String: Any]()
          try container.allKeys.forEach { (key) throws in
            result[key.stringValue] = try container.decode(AnyDecodable.self, forKey: key).value
          }
          value = result
        } else if var container = try? decoder.unkeyedContainer() {
          var result = [Any]()
          while !container.isAtEnd {
            result.append(try container.decode(AnyDecodable.self).value)
          }
          value = result
        } else if let container = try? decoder.singleValueContainer() {
          if let intVal = try? container.decode(Int.self) {
            value = intVal
          } else if let doubleVal = try? container.decode(Double.self) {
            value = doubleVal
          } else if let boolVal = try? container.decode(Bool.self) {
            value = boolVal
          } else if let stringVal = try? container.decode(String.self) {
            value = stringVal
          } else {
            throw DecodingError.dataCorruptedError(in: container, debugDescription: "the container contains nothing serialisable")
          }
        } else {
          throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Could not serialise"))
        }
      }
    }
    
    let stud = try! JSONDecoder().decode(AnyDecodable.self, from: jsonData).value as! [String: Any]
    print(stud)
    

    You could extend my struct to be AnyCodable if you are interested also in the Encoding part.

    Edit: I actually did it.

    Here is AnyCodable

    struct AnyCodable: Decodable {
      var value: Any
    
      struct CodingKeys: CodingKey {
        var stringValue: String
        var intValue: Int?
        init?(intValue: Int) {
          self.stringValue = "\(intValue)"
          self.intValue = intValue
        }
        init?(stringValue: String) { self.stringValue = stringValue }
      }
    
      init(value: Any) {
        self.value = value
      }
    
      init(from decoder: Decoder) throws {
        if let container = try? decoder.container(keyedBy: CodingKeys.self) {
          var result = [String: Any]()
          try container.allKeys.forEach { (key) throws in
            result[key.stringValue] = try container.decode(AnyCodable.self, forKey: key).value
          }
          value = result
        } else if var container = try? decoder.unkeyedContainer() {
          var result = [Any]()
          while !container.isAtEnd {
            result.append(try container.decode(AnyCodable.self).value)
          }
          value = result
        } else if let container = try? decoder.singleValueContainer() {
          if let intVal = try? container.decode(Int.self) {
            value = intVal
          } else if let doubleVal = try? container.decode(Double.self) {
            value = doubleVal
          } else if let boolVal = try? container.decode(Bool.self) {
            value = boolVal
          } else if let stringVal = try? container.decode(String.self) {
            value = stringVal
          } else {
            throw DecodingError.dataCorruptedError(in: container, debugDescription: "the container contains nothing serialisable")
          }
        } else {
          throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Could not serialise"))
        }
      }
    }
    
    extension AnyCodable: Encodable {
      func encode(to encoder: Encoder) throws {
        if let array = value as? [Any] {
          var container = encoder.unkeyedContainer()
          for value in array {
            let decodable = AnyCodable(value: value)
            try container.encode(decodable)
          }
        } else if let dictionary = value as? [String: Any] {
          var container = encoder.container(keyedBy: CodingKeys.self)
          for (key, value) in dictionary {
            let codingKey = CodingKeys(stringValue: key)!
            let decodable = AnyCodable(value: value)
            try container.encode(decodable, forKey: codingKey)
          }
        } else {
          var container = encoder.singleValueContainer()
          if let intVal = value as? Int {
            try container.encode(intVal)
          } else if let doubleVal = value as? Double {
            try container.encode(doubleVal)
          } else if let boolVal = value as? Bool {
            try container.encode(boolVal)
          } else if let stringVal = value as? String {
            try container.encode(stringVal)
          } else {
            throw EncodingError.invalidValue(value, EncodingError.Context.init(codingPath: [], debugDescription: "The value is not encodable"))
          }
    
        }
      }
    }
    

    You can test it With the previous json in this way in a playground:

    let stud = try! JSONDecoder().decode(AnyCodable.self, from: jsonData)
    print(stud.value as! [String: Any])
    
    let backToJson = try! JSONEncoder().encode(stud)
    let jsonString = String(bytes: backToJson, encoding: .utf8)!
    
    print(jsonString)
    
    0 讨论(0)
  • 2020-11-29 05:05

    Thanks to Luka Angeletti's answer (https://stackoverflow.com/a/48388443/7057338) i've changed enum to struct so we can use it more easily

    struct QuantumValue: Codable {
    
        public var string: String?
        public var integer: Int?
    
        init(from decoder: Decoder) throws {
            let container = try decoder.singleValueContainer()
            if let int = try? container.decode(Int.self) {
                self.integer = int
                return
            }
            if let string = try? container.decode(String.self) {
                self.string = string
                return
            }
            throw QuantumError.missingValue
        }
    
        func encode(to encoder: Encoder) throws {
            var container = encoder.singleValueContainer()
            try container.encode(string)
            try container.encode(integer)
        }
    
        enum QuantumError: Error {
             case missingValue
        }
    
        func value() -> Any? {
            if let s = string {
                return s
            }
            if let i = integer {
                return i
            }
            return nil
        }
    }
    
    0 讨论(0)
  • 2020-11-29 05:06

    If your problem is that it's uncertain the type of id as it might be either a string or an integer value, I can suggest you this blog post: http://agostini.tech/2017/11/12/swift-4-codable-in-real-life-part-2/

    Basically I defined a new Decodable type

    public struct UncertainValue<T: Decodable, U: Decodable>: Decodable {
        public var tValue: T?
        public var uValue: U?
    
        public var value: Any? {
            return tValue ?? uValue
        }
    
        public init(from decoder: Decoder) throws {
            let container = try decoder.singleValueContainer()
            tValue = try? container.decode(T.self)
            uValue = try? container.decode(U.self)
            if tValue == nil && uValue == nil {
                //Type mismatch
                throw DecodingError.typeMismatch(type(of: self), DecodingError.Context(codingPath: [], debugDescription: "The value is not of type \(T.self) and not even \(U.self)"))
            }
    
        }
    }
    

    From now on, your Person object would be

    struct Person: Decodable {
        var id: UncertainValue<Int, String>
    }
    

    you will be able to access your id using id.value

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

    Codable needs to know the type to cast to.

    Firstly I would try to address the issue of not knowing the type, see if you can fix that and make it simpler.

    Otherwise the only way I can think of solving your issue currently is to use generics like below.

    struct Person<T> {
        var id: T
        var name: String
    }
    
    let person1 = Person<Int>(id: 1, name: "John")
    let person2 = Person<String>(id: "two", name: "Steve")
    
    0 讨论(0)
  • 2020-11-29 05:12

    You can replace Any with an enum accepting an Int or a String:

    enum Id: Codable {
        case numeric(value: Int)
        case named(name: String)
    }
    
    struct Person: Codable
    {
        var id: Id
    }
    

    Then the compiler will complain about the fact that Id does not conform to Decodable. Because Id has associated values you need to implement this yourself. Read https://littlebitesofcocoa.com/318-codable-enums for an example of how to do this.

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

    Quantum Value

    First of all you can define a type that can be decoded both from a String and Int value. Here it is.

    enum QuantumValue: Decodable {
        
        case int(Int), string(String)
        
        init(from decoder: Decoder) throws {
            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 QuantumError.missingValue
        }
        
        enum QuantumError:Error {
            case missingValue
        }
    }
    

    Person

    Now you can define your struct like this

    struct Person: Decodable {
        let id: QuantumValue
    }
    

    That's it. Let's test it!

    JSON 1: id is String

    let data = """
    {
    "id": "123"
    }
    """.data(using: String.Encoding.utf8)!
    
    if let person = try? JSONDecoder().decode(Person.self, from: data) {
        print(person)
    }
    

    JSON 2: id is Int

    let data = """
    {
    "id": 123
    }
    """.data(using: String.Encoding.utf8)!
    
    if let person = try? JSONDecoder().decode(Person.self, from: data) {
        print(person)
    }
    

    UPDATE 1 Comparing values

    This new paragraph should answer the questions from the comments.

    If you want to compare a quantum value to an Int you must keep in mind that a quantum value could contain an Int or a String.

    So the question is: what does it mean comparing a String and an Int?

    If you are just looking for a way of converting a quantum value into an Int then you can simply add this extension

    extension QuantumValue {
        
        var intValue: Int? {
            switch self {
            case .int(let value): return value
            case .string(let value): return Int(value)
            }
        }
    }
    

    Now you can write

    let quantumValue: QuantumValue: ...
    quantumValue.intValue == 123
    

    UPDATE 2

    This part to answer the comment left by @Abrcd18.

    You can add this computed property to the Person struct.

    var idAsString: String {
        switch id {
        case .string(let string): return string
        case .int(let int): return String(int)
        }
    }
    

    And now to populate the label just write

    label.text = person.idAsString
    

    Hope it helps.

    0 讨论(0)
提交回复
热议问题