Adopting Customnserror in Decodingerror

Adopting CustomNSError in DecodingError

NSError bridging is an interesting beast in the Swift compiler. On the one hand, NSError comes from the Foundation framework, which your application may or may not use; on the other, the actual bridging mechanics need to be performed in the compiler, and rightfully, the compiler should have as little knowledge of "high-level" libraries above the standard library as possible.

As such, the compiler has very little knowledge of what NSError actually is, and instead, Error exposes three properties which provide the entirety of the underlying representation of NSError:

public protocol Error {
var _domain: String { get }
var _code: Int { get }

// Note: _userInfo is always an NSDictionary, but we cannot use that type here
// because the standard library cannot depend on Foundation. However, the
// underscore implies that we control all implementations of this requirement.
var _userInfo: AnyObject? { get }

// ...
}

NSError, then, has a Swift extension which conforms to Error and implements those three properties:

extension NSError : Error {
@nonobjc
public var _domain: String { return domain }

@nonobjc
public var _code: Int { return code }

@nonobjc
public var _userInfo: AnyObject? { return userInfo as NSDictionary }

// ...
}

With this, when you import Foundation, any Error can be cast to an NSError and vice versa, as both expose _domain, _code, and _userInfo (which is what the compiler actually uses to perform the bridging).

The CustomNSError protocol plays into this by allowing you to supply an errorDomain, errorCode, and errorUserInfo, which are then exposed by various extensions as their underscore versions:

public extension Error where Self : CustomNSError {
/// Default implementation for customized NSErrors.
var _domain: String { return Self.errorDomain }

/// Default implementation for customized NSErrors.
var _code: Int { return self.errorCode }

// ...
}

So, how are EncodingError and DecodingError different? Well, since they're both defined in the standard library (which is present regardless of whether or not you use Foundation, and cannot depend on Foundation), they hook into the system by providing implementations of _domain, _code, and _userInfo directly.

Since both types provide the direct underscore versions of those variables, they don't call in to the non-underscore versions to get the domain, code, and user info — the values are used directly (rather than rely on var _domain: String { return Self.errorDomain }).

So, in effect, you can't override the behavior because EncodingError and DecodingError already provide this info. Instead, if you want to provide different codes/domains/user info dictionaries, you're going to need to write a function which takes an EncodingError/DecodingError and returns your own NSError, or similar.

Swift 3: How to catch error if NSCoder decodeX function decodes wrong value type?

Using decodeInteger on a non-integer key would raise an exception. Sadly, it's an NSException which Swift cannot handle directly (see references below).

You need to first write a wrapper to handle ObjC exceptions in ObjC and bridge it over to Swift (inspired by this answer):

/// -------------------------------------------
/// ObjC.h
/// -------------------------------------------
#import <Foundation/Foundation.h>

@interface ObjC : NSObject

+ (BOOL)catchException:(void(^)())tryBlock error:(__autoreleasing NSError **)error;

@end

/// -------------------------------------------
/// ObjC.m
/// -------------------------------------------
#import "ObjC.h"

@implementation ObjC

+ (BOOL)catchException:(void(^)())tryBlock error:(__autoreleasing NSError **)error {
@try {
tryBlock();
return YES;
}
@catch (NSException *exception) {
NSMutableDictionary * userInfo = [NSMutableDictionary dictionaryWithDictionary:exception.userInfo];
[userInfo setValue:exception.reason forKey:NSLocalizedDescriptionKey];
[userInfo setValue:exception.name forKey:NSUnderlyingErrorKey];

*error = [[NSError alloc] initWithDomain:exception.name
code:0
userInfo:userInfo];
return NO;
}
}

@end

Now you can catch the exception in Swift:

do {
try ObjC.catchException {
let age = aDecoder.decodeInteger(forKey: "firstName")
}
} catch {
print(error.localizedDescription)
}

References: Using ObjectiveC with Swift: Adopting Cocoa Design Patterns

Although Swift error handling resembles exception handling in Objective-C, it is entirely separate functionality. If an Objective-C method throws an exception during runtime, Swift triggers a runtime error. There is no way to recover from Objective-C exceptions directly in Swift. Any exception handling behavior must be implemented in Objective-C code used by Swift.

Asymmetric Encoding/Decoding with Codable and Firestore?

I don't get where you are seeing that error from - a line reference would be useful - but it's not directly related to your en/decoding. (As an aside, that method really is not a data model)

As you want to decode both properties with JSON keys that match their property names, there is no need to specify CodingKeys or write a custom decoder; you can rely on the synthesised decoder and let Codable do the work for you.

For the decoding you will need a custom solution else Decodable will decode both fields. This will require both a CodingKey enum (note the singular, i.e. the protocol, not the defaut enun name) and a custom encoder to use that.

You end up with a far simpler implementation of your struct. I've also added a simple initialiser as you lose the synthesised memberwise initialiser as soon as you define the init(from:). This was just so I could test it.

struct Recipe: Codable {
var id: String?
var vegetarian: Bool?

init(id: String, vegetarian: Bool){
self.id = id
self.vegetarian = vegetarian
}

init(from decoder: Decoder) throws {

enum DecodingKeys: CodingKey {
case vegetarian
}

let container = try decoder.container(keyedBy: DecodingKeys.self)
vegetarian = try container.decode(Bool.self, forKey: .vegetarian)
}
}

If you test this you will find that it will just decode the vegetarian property but encode both. Simple testing shows:

let recipe = Recipe(id: "1", vegetarian: true)
let data = try! JSONEncoder().encode(recipe)
print(String(data: data, encoding: .utf8)!) //{"id":"1","vegetarian":true}

let decodedRecipe = try! JSONDecoder().decode(Recipe.self, from: data)
print(decodedRecipe) //Recipe(id: nil, vegetarian: Optional(true))

Why does my special Codable protocol work differently than Swift's Codable with Array?

The error message might be more useful if it used the unsugared typename:

Type 'Array<Person>' has no member 'decode';

Person may conform to your protocol, but Array does not. Swift explicitly declares that Arrays are Decodable if their elements are. You just need to do the same:

extension Array : JsonDecodable where Element : JsonDecodable {
static func decode(data: Data?, decoder: JSONDecoder) -> Self? {
// Decode each element and return an array
}
}

This uses a feature called "Conditional Conformance", which allows containers generally to conform to a protocol if the type they hold also does.



Related Topics



Leave a reply



Submit