Add JSONDecoder decode function with JSONObject

Introduction

Add a function to JSONDecoder to decode with json object
func decode<T>(T.Type, from jsonObject: Any) -> T

Motivation

When we decode json with JSONDecoder, we can decode json only use json data with
func decode<T>(T.Type, from data: Data) -> T.
Sometimes, We need interface like func decode<T>(T.Type, from jsonObject: Any) -> T to decode json with json object.
I know we can use that method with converting json object to json data using JSONSerialization

let jsonData = try! JSONSerialization.data(withJSONObject obj: jsonObject)
let object = try! JSONDecoder().decode(Object.self, from: jsonData)

But, I think that's inconvenient and unneeded step.

Proposed solution

Fortunatly, func decode<T>(T.Type, from data: Data) -> T method converts json data to json object.

// MARK: - Decoding Values
    /// Decodes a top-level value of the given type from the given JSON representation.
    ///
    /// - parameter type: The type of the value to decode.
    /// - parameter data: The data to decode from.
    /// - returns: A value of the requested type.
    /// - throws: `DecodingError.dataCorrupted` if values requested from the payload are corrupted, or if the given data is not valid JSON.
    /// - throws: An error if any value throws an error during decoding.
    open func decode<T : Decodable>(_ type: T.Type, from data: Data) throws -> T {
        let topLevel: Any
        do {
            topLevel = try JSONSerialization.jsonObject(with: data, options: .allowFragments)
        } catch {
            throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "The given data was not valid JSON.", underlyingError: error))
        }

        let decoder = _JSONDecoder(referencing: topLevel, options: self.options)
        guard let value = try decoder.unbox(topLevel, as: type) else {
            throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: [], debugDescription: "The given data did not contain a top-level value."))
        }

        return value
    }

Detailed design

So, I suggest to JSONDecoder to add an interface to decode with json object.

open func decode<T : Decodable>(_ type: T.Type, from jsonObject: Any) throws -> T {
// Maybe JSONSerialization.isValidJSONObject is unneeded. Because If jsonObject isn't valid JSONObject, decoding will be failed. 
    guard JSONSerialization.isValidJSONObject(jsonObject) else {
        throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "The given data was not valid JSON.", underlyingError: error))
    }
    
    let decoder = _JSONDecoder(referencing: jsonObject, options: self.options)
    guard let value = try decoder.unbox(jsonObject, as: type) else {
        throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: [], debugDescription: "The given data did not contain a top-level value."))
    }
    
    return value
}

Source compatibility

I think that solution is simple and easy to implementation.
And It will be helpfult when we need decode two jsonobject to one model.
We don't have to three models to decode two jsonobject to one model.

/// When we decode two json data with func decode<T>(T.Type, from data: Data) -> T
let object1 = try! JSONDecoder().decode(Object1.self, from: jsonData)
let object2 = try! JSONDecoder().decode(Object2.self, from: jsonData)
let obejct = Object.init(object1, object2)

/// If JSONDecoder privides  decode<T : Decodable>(_ type: T.Type, from jsonObject: Any) throws -> T  We can define only one model.
jsonObject2.forEach {
    jsonObject1[$0.key] = $0.value
}

let object = try! JSONDecoder().decode(Object.self, from: jsonObject1) // Isn't it simple??
2 Likes

Another potential uses case is working with WKScriptMessageHandler. The message provided to the function is already a Any (e.g., WKScriptMessage.body). It would be nice to be able to skip turning the Any into Data and then decode from there.

Out of curiosity did this, or something similar to this ever move forward?