1

I have function that parse incoming json and return handy error while it can't. However, when it comes to invalid JSON app crashes on line:

let data = try? JSONSerialization.data(withJSONObject: result),

The complete functions is:

static func parseModel<Response: Decodable>(from response: Any?, with error: Error? = nil) -> Result<Response, Error> {
            guard let result = response,
                let data = try? JSONSerialization.data(withJSONObject: result),
                  let model = try? JSONDecoder().decode(Response.self, from: data)
            else { return .failure(error ?? APIError.emptyResponse) }
    
            return .success(model)
        }

How it that possible that function doesn't produce .failure(error ?? APIError.emptyResponse) but crashes instead?

Evgeniy Kleban
  • 6,794
  • 13
  • 54
  • 107
  • 3
    What is the exception reason? And strictly spoken the function doesn't throw since you ignore the errors. – vadian Mar 13 '23 at 15:18
  • @vadian it says - Invalid top-level type in JSON write, something with wrong data from server, but, it suppose to fall in return .failure(error ?? APIError.emptyResponse) – Evgeniy Kleban Mar 13 '23 at 15:31
  • @vadian open class func data(withJSONObject obj: Any, options opt: JSONSerialization.WritingOptions = []) throws -> Data is throwable function, but i did manage with try? operator to get not-falling results – Evgeniy Kleban Mar 13 '23 at 15:33
  • You can test this: `do { let data = try JSONSerialization.data(withJSONObject: UIView()) } catch { print("Error: \(error)") }`. It won't go on the `catch`, because it's not a "thrown error", it's a `NSException`, it's different. – Larme Mar 13 '23 at 15:45
  • See https://stackoverflow.com/questions/38737880/uncaught-error-exception-handling-in-swift – Larme Mar 13 '23 at 15:46

1 Answers1

4

As the documentation for JSONSerialization method data(withJSONObject:options:) says:

If obj can’t produce valid JSON, JSONSerialization throws an exception. This exception occurs prior to parsing and represents a programming error, not an internal error. Before calling this method, you should check whether the input can produce valid JSON by using isValidJSONObject(_:).

So, it would appear that your parameter, response, is being supplied something that is not a valid JSON object, insofar as JSONSerialization can interpret it. Thus it throws an exception that must be resolved during the design/development process, and should not be confused with errors that can be programmatically thrown or caught.

Regards why the exception is occurring, it is becauseJSONSerialization only allows a narrow selection of types:

  • The top level object is an NSArray or NSDictionary
  • All objects are instances of NSString, NSNumber, NSArray, NSDictionary, or NSNull.
  • All dictionary keys are instances of NSString.
  • Numbers are neither NaN or infinity.

You must be using some type that is not understood by JSONSerialization. Likely candidates include optionals or some custom type. But we cannot be sure which applies in your case without a reproducible example.

So, look at your console to see if the exception provides illuminating information. Do not focus on the hairy looking stack trace, but see if there are any useful diagnostic messages immediately before or after that.


I might also advise that you avoid try? and use try instead, wrapping this all in a do-catch block. Even if you choose to not return that error for some reason, you can at least print it so you do not lose all useful diagnostic information:

static func parseModel<Response: Decodable>(from response: Any?, with error: Error? = nil) -> Result<Response, Error> {
    do {
        guard let response else {
            return .failure(error ?? APIError.emptyResponse) 
        }

        let data = try JSONSerialization.data(withJSONObject: response)
        let model = try JSONDecoder().decode(Response.self, from: data)

        return .success(model)
    } catch let parseError {
        print(parseError)
        return .failure(error ?? parseError)         // maybe just `.failure(parseError)`
    }
}

Theoretically, one could include a test for isValidJSONObject, but this is generally a programming error that should be remedied, not one that should merely be silenced with a isValidJSONObject test.

That is the whole reason they consciously made it throw an exception rather merely being an error. There are some edge-cases where you might use isValidJSONObject (e.g., you are developing a library or service and you want to gracefully inform the caller of the misuse of your function with a custom error), but within our own codebases, it is generally better to avail ourselves of the exception so we can quickly identify and resolve the programming mistake early in the development process.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • 1
    Upvoted for the improved error handling but shouldn't your example include a call to `isValidJSONObject`? – Joakim Danielson Mar 13 '23 at 15:59
  • Thank you for your answer, though i only fix my issue by adding JSONSerialization.isValidJSONObject(result), to guard chain – Evgeniy Kleban Mar 13 '23 at 16:16
  • 1
    @JoakimDanielson - I confess that was my inclination, too, and my first answer actually included a test for `isValidJSONObject`. But I removed it because I realized that this is a programming error that should be remedied, not one merely silenced with a `isValidJSONObject` test. That’s the whole reason they made it throw an exception rather including this invalid data type exception in the standard error handling. There are some edge-cases where you might use `isValidJSONObject`, which is why they even have that function, but to my eye at least, this did not seem like a good use-case. – Rob Mar 13 '23 at 16:22