Reputation: 1163
Given this struct:
public struct Error: Codable {
public let code: String
public let message: String
public let params: [String: String]?
}
And this JSON:
[
{
"message" : "The requested user could not be found.",
"params" : [],
"code" : "requested_user_not_found"
}
]
Is there a way to decode this using the JSONDecoder() class in Swift? The params
key is supposed to be a dictionary, but due to the way the external API which produces this JSON is implemented (in PHP), empty dictionaries are rendered in JSON as empty arrays.
At the moment, attempting to decode the provided JSON into an instance of the provided struct results in an error being thrown.
Upvotes: 4
Views: 1729
Reputation: 299275
First, PHP doesn't need to do this, so if possible the PHP should be corrected. The way to express "empty object" in PHP is new \stdClass()
. Elastic has a nice explanation.
That said, if you cannot correct the server, you can fix it on the client side. Many of the answers here are based on trying to decode the value, and if failing assuming it's an empty array. That works, but it means that unexpected JSON will not generate good errors. Instead, I'd extract this problem into a function:
/// Some PHP developers emit [] to indicate empty object rather than using stdClass().
/// This returns a default value in that case.
extension KeyedDecodingContainer {
func decodePHPObject<T>(_ type: T.Type, forKey key: Key, defaultValue: T) throws -> T
where T : Decodable
{
// Sadly neither Void nor Never conform to Decodable, so we have to pick a random type here, String.
// The nicest would be to decode EmptyCollection, but that also doesn't conform to Decodable.
if let emptyArray = try? decode([String].self, forKey: key), emptyArray.isEmpty {
return defaultValue
}
return try decode(T.self, forKey: key)
}
// Special case the common dictionary situation.
func decodePHPObject<K, V>(_ type: [K: V].Type, forKey key: Key) throws -> [K: V]
where K: Codable, V: Codable
{
return try decodePHPObject([K:V].self, forKey: key, defaultValue: [:])
}
}
This provides a .decodePHPObject(_:forKey:)
method that you can the use in a custom decoder.
public struct ErrorValue: Codable {
public let code: String
public let message: String
public let params: [String: String]
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.code = try container.decode(String.self, forKey: .code)
self.message = try container.decode(String.self, forKey: .message)
// And use our new decodePHPObject.
self.params = try container.decodePHPObject([String: String].self, forKey: .params)
}
}
(I've renamed this ErrorValue
to remove the conflict with the stdlib type Error
, and I've made params
non-optional since you generally should not have optional collections unless "empty" is going to be treated differently than nil.)
Upvotes: 2
Reputation: 3886
Try the snippet below, here you will be trying to decode the params as dictionary. If it fails we will be assigning an empty dictionary to it.
public struct Error: Codable {
public let code: String
public let message: String
public let params: [String:String]
public init(from decoder: Decoder) throws {
let keyedContainer = try decoder.container(keyedBy: CodingKeys.self)
self.code = try keyedContainer.decode(String.self, forKey: .code)
self.message = try keyedContainer.decode(String.self, forKey: .message)
if let params = try? keyedContainer.decodeIfPresent([String: String].self, forKey: .params) {
self.params = params
} else {
self.params = [:]
}
}
}
Hope this helps.
Upvotes: 1