How to use Any in Codable Type
Solution 1
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")
Solution 2
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.
Solution 3
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)
Solution 4
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
Solution 5
Simply you can use AnyCodable
type from Matt Thompson's cool library AnyCodable.
Eg:
import AnyCodable
struct Person: Codable
{
var id: AnyCodable
}
Related videos on Youtube
Comments
-
PGDev about 2 years
I'm currently working with
Codable
types in my project and facing an issue.struct Person: Codable { var id: Any }
id
in the above code could be either aString
or anInt
. This is the reasonid
is of typeAny
.I know that
Any
is notCodable
.What I need to know is how can I make it work.
-
Dávid Pásztor over 6 yearsRelated: Swift structures handling multiple tapes for a single property. In summary: you shouldn’t use ‘Any’, but have 2 optional properties (one of type ‘String’ and one ‘Int’ in your case) and try decoding the JSON value as both. Moreover, your case is actually quite simple, since ‘Int’ can always be converted to ‘String’.
-
Rob Napier over 6 yearsThe linked answer also answers this question, even if you use
Any
. You shouldn't useAny
, you should use an enum, but the approach still works exactly the same way forAny
; just manually decode from the container and see if it works. If not, move on to the next type. -
matt over 6 yearsThis sort of thing has been answered many times. The Int-Or-String problem, for example, is neatly solved here: stackoverflow.com/a/47215561/341994 See for example also stackoverflow.com/questions/46392046/… as well as e.g. stackoverflow.com/questions/44603248/… To open a bounty without searching adequately is kind of a waste.
-
halfer about 6 years@matt: can this be closed as a dup?
-
PGDev about 6 years@halfer I don't think it's a dup. None of the previously asked questions could answer the queries we have here. Also, none of questions could be answer in such numerous ways .
-
-
PGDev over 6 yearsIn your approach to use generics, I must still know the data type of
id
that I am getting fromJSON
. -
Scriptable over 6 yearsyes, hence the first sentence of my answer. If you do not KNOW the type you cannot use codable. you will need to try conditional unwrapping. Codable must know the type.
-
Natural Lam over 6 yearsThanks Giuseppe! This is the most practical answer even though it's not truly 'AnyCodable', because Any can only be primitive types here (not other custom Codable types), but it should be good enough for most of the use cases... Btw, why don't you include other types like Date, Data, URL which are also natively supported?
-
Giuseppe Lanza over 6 yearsBecause in Json they are strings. To know they represent date data and URL we would need insights that with "any" we don't have. :/
-
Natural Lam over 6 yearsI see.. For my use case as I use both encode & decode as a pair from the app (i.e. the backend data always saved from the app as well), I think it should just work with the default date/data/url scheme that Swift use
-
dvp.petrov about 6 yearsFirstly, let me say that, this solution is pretty smart and works super for the asked question. Something you could add to your answer is later on usage of the property:
if case let QuantumValue.string(s) = person { print("id is: \(s)") }
. If we go a little bit further, it is horrible to do something like this from logic perspective of the code. On my opinion, the better solution is to communicate a change in the response object so it is consistent with only a single type. -
n8tr about 6 yearsThis works perfectly. Nice work. Ironically though, in the end, I think doing this is pointless. When you end up wanting to use the parsed data somewhere, you need to know where that data is within the structure and what type it is. So if you are going to go through that optional chain and casting, you might as well have defined it in standard Codable objects in the first place. Just don't parse the data you don't know about, which is easily achieved in Codable by omitting keys.
-
J. Doe almost 6 yearsThis line: 'if let string' would be better if it would be 'else if let'.
-
Nick over 5 yearsCode-only answers are discouraged. Please click on edit and add some words summarising how your code addresses the question, or perhaps explain how your answer differs from the previous answer/answers. Thanks
-
Dhanunjay Kumar about 5 yearsi have a doubt that how can we compare (person.id == 123) i am not able to compare is there any way to convert QuantumValue to int? @Luca Angeletti
-
King about 5 years@DhanunjayKumar did you figure this out?
-
Dhanunjay Kumar about 5 years@King no i am not able to compare quantumValue to int or double . so i am converting int to double while parsing. any clue how to compare?
-
Luca Angeletti about 5 years@DhanunjayKumar Please check the new paragraph in my answer
-
Dani almost 4 yearshow is this different from the already accepted answer? You literally copy pasted that. This is wrong on so many levels...
-
BlaShadow almost 4 yearsI had the same issue with a backend with an inconsistency in the data type, and this is the answer that helps me, also nice naming.
-
Abrcd18 over 3 yearsHello! I followed your answer and faced the following problem. Text on label is printed as "int(1919)" and "double(3939.2)". Could you please say what should I do to remove "int" and "double" before numbers?
-
Luca Angeletti over 3 yearsHey @Abrcd18, have a look at the update I added to my answer.
-
Mad Man over 3 years"wrong on so many levels" what you mean? It's different.
-
DrMickeyLauer about 3 yearsThis is very interesting, @GiuseppeLanza. But is there a way to improve decoding by automatically transform [Any] into [<KnownType>], if we see a homogenous array?
-
Albi about 3 yearsWriting this part inside the enum solved my issue var any:Any{ get{ switch self { case .double(let value): return value case .int(let value): return value case .string(let value): return value } } }
-
matteoh almost 3 yearsInteresting, but let's say id is a String, how can I convert / parse it to a String, since
id as String
won't work? -
barola_mes over 2 years@GiuseppeLanza this is great, thank you for that piece of code but the encode() does not work for Codable structs.
-
karthikeyan over 2 years@LucaAngeletti How to update the quantum value? I want to send same model data to server with updated value..assume that "flag" (by default 0 from server ) is Quantum in model(let flag : QuantumValue?) now i want to change the value to 1
-
Luca Angeletti over 2 years@karthikeyan
YourDataModel(flag: .int(1))
-
karthikeyan over 2 years@LucaAngeletti Thanks for your comment..i fixed it already, someone might get useful...Thank you so much