Decoding a JSON Field that Could Be String or Double with Swift
Codable provides an elegant and declarative way of decoding a JSON in Swift. We just need to define a Type that matches the structure of the JSON.
Unfortunately, we don’t live in a perfect world so as an iOS Developer, you could need to decode a JSON with a field that doesn’t always have the same type.
Something like this
{
"movies": [
{
"title": "Start Trek: Generations",
"vote": 6.6
},
{
"title": "Start Trek: First Contact",
"vote": "7.6"
}
]
}
As you can see the vote
field sometimes contains a Double
"vote": 6.6
and sometimes a String
.
"vote": "7.6"
This is an error from who is providing the JSON but sometimes you just need to deal with it. So here’s a quick solution to solve this problem using Codable.
The Movies struct
First of all, let’s define a Swift struct to match the JSON structure.
struct Movies: Decodable { let movies: [Movie]
struct Movie: Decodable {
let title: String
let vote: 🤔🤔🤔
}
}
Good, now we need to figure out which type to use for the vote
field!
Since we know that it could contain a String
or a Double
we can define an Enum for that.
The StringOrDouble Enum
Here’s the core idea of this article. This enum is a Decodable
Type that can match a string
or a double
found in the JSON.
enum StringOrDouble: Decodable {
case string(String)
case double(Double)
init(from decoder: Decoder) throws {
if let double = try? decoder.singleValueContainer().decode(Double.self) {
self = .double(double)
return
} if let string = try? decoder.singleValueContainer().decode(String.self) {
self = .string(string)
return
} throw Error.couldNotFindStringOrDouble
} enum Error: Swift.Error {
case couldNotFindStringOrDouble
}
}
How does it work?
The Enum defines 2 cases each one with an associated value.
The string
case will contain a String
value. And the double
case will contain… well a Double
.
case string(String)
case double(Double)
Next, we provide a custom init
which reads the value from the JSON and try to convert it first into a Double
.
If the conversion succeeds then we get a value of type StringOrDouble.double
where the associated value if the number found in the JSON.
If the conversion fails then we try to read a String
to produce a value of type StringOrDouble.string
.
If also the second conversion fails then an error is thrown.
Back to the Movies model
Now we can complete the implementation of the Movies
model
struct Movies: Decodable { let movies: [Movie]
struct Movie: Decodable {
let title: String
let vote: StringOrDouble
}
}
Testing
Finally, we can decode our JSON.
let data = """
{
"movies": [
{
"title": "Start Trek: Generations",
"vote": 6.6
},
{
"title": "Start Trek: First Contact",
"vote": "7.6"
}
]
}
""".data(using: .utf8)!do {
let movies = try JSONDecoder().decode(Movies.self, from: data)
print(movies)
} catch {
print(error)
}
Result
Movies(movies: [
Movies.Movie(title: "Start Trek: Generations",
vote: StringOrDouble.double(6.6)),
Movies.Movie(title: "Start Trek: First Contact",
vote: StringOrDouble.string("7.6"))
])