I'm having an inconsistent API that might return either a String
or an Number
as a part of the JSON response.
The dates also could be represented the same way as either a String
or a Number
, but are always an UNIX timestamp (i.e. timeIntervalSince1970
).
To fix the issue with the dates, I simply used a custom JSONDecoder.DateDecodingStrategy
:
decoder.dateDecodingStrategy = JSONDecoder.DateDecodingStrategy.custom({ decoder in
let container = try decoder.singleValueContainer()
if let doubleValue = try? container.decode(Double.self) {
return Date(timeIntervalSince1970: doubleValue)
} else if let stringValue = try? container.decode(String.self),
let doubleValue = Double(stringValue) {
return Date(timeIntervalSince1970: doubleValue)
}
throw DecodingError.dataCorruptedError(in: container,
debugDescription: "Unable to decode value of type `Date`")
})
However, no such customization is available for the Int
or Double
types which I'd like to apply it for.
So, I have to resort to writing Codable
initializers for each of the model types that I'm using.
The alternative approach I'm looking for is to subclass the JSONDecoder
and override the decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable
method.
In that method I'd like to "inspect" the type T
that I'm trying to decode to and then, if the base implementation (super
) fails, try to decode the value first to String
and then to the T
(the target type).
So far, my initial prototype looks like this:
final class CustomDecoder: JSONDecoder {
override func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable {
do {
return try super.decode(type, from: data)
} catch {
if type is Int.Type {
print("Trying to decode as a String")
if let decoded = try? super.decode(String.self, from: data),
let converted = Int(decoded) {
return converted as! T
}
}
throw error
}
}
}
However, I found out that the "Trying to decode as a String"
message is never printed for some reason, even though the control reaches the catch
stage.
I'm happy to have that custom path only for Int
and Double
types, since the T
is Codable
and that doesn't guarantee ability to initialize a value with the String
, however, I of course welcome a more generalized approach.
Here's the sample Playground code that I came up with to test my prototype. It can be copy-pasted directly into the Playground and works just fine.
My goal is to have both jsonsample1
and jsonsample2
to produce the same result.
import UIKit
final class CustomDecoder: JSONDecoder {
override func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable {
do {
return try super.decode(type, from: data)
} catch {
if type is Int.Type {
print("Trying to decode as a String")
if let decoded = try? super.decode(String.self, from: data),
let converted = Int(decoded) {
return converted as! T
}
}
throw error
}
}
}
let jsonSample1 =
"""
{
"name": "Paul",
"age": "38"
}
"""
let jsonSample2 =
"""
{
"name": "Paul",
"age": 38
}
"""
let data1 = jsonSample1.data(using: .utf8)!
let data2 = jsonSample2.data(using: .utf8)!
struct Person: Codable {
let name: String?
let age: Int?
}
let decoder = CustomDecoder()
let person1 = try? decoder.decode(Person.self, from: data1)
let person2 = try? decoder.decode(Person.self, from: data2)
print(person1 as Any)
print(person2 as Any)
What could be the reason for my CustomDecoder
not working?