Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Allow JSONDecoder() to accept empty array as empty dictionary

Tags:

json

ios

swift

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.

like image 906
Liam W Avatar asked Oct 15 '25 18:10

Liam W


1 Answers

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.)

like image 126
Rob Napier Avatar answered Oct 18 '25 10:10

Rob Napier



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!