Using codable with value that is sometimes an Int and other times a String
Seamlessly decoding from either Int
or String
into the same property requires writing some code.
However, thanks to a (somewhat) new addition to the language,(property wrappers), you can make it quite easy to reuse this logic wherever you need it:
// note this is only `Decodable`
struct GeneralProduct: Decodable {
var price: Double
@Flexible var id: Int // note this is an Int
var name: String
}
The property wrapper and its supporting code can be implemented like this:
@propertyWrapper struct Flexible<T: FlexibleDecodable>: Decodable {
var wrappedValue: T
init(from decoder: Decoder) throws {
wrappedValue = try T(container: decoder.singleValueContainer())
}
}
protocol FlexibleDecodable {
init(container: SingleValueDecodingContainer) throws
}
extension Int: FlexibleDecodable {
init(container: SingleValueDecodingContainer) throws {
if let int = try? container.decode(Int.self) {
self = int
} else if let string = try? container.decode(String.self), let int = Int(string) {
self = int
} else {
throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath, debugDescription: "Invalid int value"))
}
}
}
Original answer
You can use a wrapper over a string that knows how to decode from any of the basic JSON data types: string, number, boolean:
struct RelaxedString: Codable {
let value: String
init(_ value: String) {
self.value = value
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
// attempt to decode from all JSON primitives
if let str = try? container.decode(String.self) {
value = str
} else if let int = try? container.decode(Int.self) {
value = int.description
} else if let double = try? container.decode(Double.self) {
value = double.description
} else if let bool = try? container.decode(Bool.self) {
value = bool.description
} else {
throw DecodingError.typeMismatch(String.self, .init(codingPath: decoder.codingPath, debugDescription: ""))
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(value)
}
}
You can then use this new type in your struct. One minor disadvantage would be that consumer of the struct will need to make another indirection to access the wrapped string. However that can be avoided by declaring the decoded RelaxedString
property as private, and use a computed one for the public interface:
struct GeneralProduct: Codable {
var price: Double!
var _id: RelaxedString?
var name: String!
var id: String? {
get { _id?.value }
set { _id = newValue.map(RelaxedString.init) }
}
private enum CodingKeys: String, CodingKey {
case price = "p"
case _id = "i"
case name = "n"
}
init(price: Double? = nil, id: String? = nil, name: String? = nil) {
self.price = price
self._id = id.map(RelaxedString.init)
self.name = name
}
}
Advantages of the above approach:
- no need to write custom
init(from decoder: Decoder)
code, which can become tedious if the number of properties to be decoded increase - reusability -
RelaxedString
can be seamlessly used in other structs - the fact that the id can be decoded from a string or an int remains an implementation detail, consumers of
GeneralProduct
don't know/care that the id can come from a string or an int - the public interface exposes string values, which keeps the consumer code simple as it will not have to deal with multiple types of data
This is a possible solution with MetadataType
, the nice thing is that can be a general solution not for GeneralProduct
only, but for all the struct
having the same ambiguity:
struct GeneralProduct: Codable {
var price:Double?
var id:MetadataType?
var name:String?
private enum CodingKeys: String, CodingKey {
case price = "p"
case id = "i"
case name = "n"
}
init(price:Double? = nil, id: MetadataType? = nil, name: String? = nil) {
self.price = price
self.id = id
self.name = name
}
}
enum MetadataType: Codable {
case int(Int)
case string(String)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
self = try .int(container.decode(Int.self))
} catch DecodingError.typeMismatch {
do {
self = try .string(container.decode(String.self))
} catch DecodingError.typeMismatch {
throw DecodingError.typeMismatch(MetadataType.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Encoded payload not of an expected type"))
}
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .int(let int):
try container.encode(int)
case .string(let string):
try container.encode(string)
}
}
}
this is the test:
let decoder = JSONDecoder()
var json = "{\"p\":2.19,\"i\":0,\"n\":\"Black Shirt\"}"
var product = try! decoder.decode(GeneralProduct.self, from: json.data(using: .utf8)!)
if let id = product.id {
print(id) // 0
}
json = "{\"p\":2.19,\"i\":\"hello world\",\"n\":\"Black Shirt\"}"
product = try! decoder.decode(GeneralProduct.self, from: json.data(using: .utf8)!)
if let id = product.id {
print(id) // hello world
}
struct GeneralProduct: Codable {
var price: Double?
var id: String?
var name: String?
private enum CodingKeys: String, CodingKey {
case price = "p", id = "i", name = "n"
}
init(price: Double? = nil, id: String? = nil, name: String? = nil) {
self.price = price
self.id = id
self.name = name
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
price = try container.decode(Double.self, forKey: .price)
name = try container.decode(String.self, forKey: .name)
do {
id = try String(container.decode(Int.self, forKey: .id))
} catch DecodingError.typeMismatch {
id = try container.decode(String.self, forKey: .id)
}
}
}
let json1 = """
{
"p":2.12,
"i":"3k3mkfnk3",
"n":"Blue Shirt"
}
"""
let json2 = """
{
"p":2.12,
"i":0,
"n":"Blue Shirt"
}
"""
do {
let product = try JSONDecoder().decode(GeneralProduct.self, from: Data(json2.utf8))
print(product.price ?? "nil")
print(product.id ?? "nil")
print(product.name ?? "nil")
} catch {
print(error)
}
edit/update:
You can also simply assign nil
to your id
when your api returns 0
:
do {
let value = try container.decode(Int.self, forKey: .id)
id = value == 0 ? nil : String(value)
} catch DecodingError.typeMismatch {
id = try container.decode(String.self, forKey: .id)
}