'NSInvalidArgumentException', reason: 'Invalid type in JSON write (__SwiftValue)'

Hello, I am trying to understand a crash in JSONSerialization that I am running into with a third-party event tracking SDK.

Playground

struct SimpleObject {
    let name: String
    let age: Int
}

func checkEncoding() {
    let obj = SimpleObject(name: "Hello", age: 30)
    let eventDict: [String: Any] = ["obj": obj]

    // NSJSONSerialization (what Sprig uses internally)
    print("\n--- NSJSONSerialization Test ---")
    do {
        _ = try JSONSerialization.data(withJSONObject: eventDict)
        print("âś… NSJSONSerialization succeeded")
    } catch {
        print("❌ NSJSONSerialization failed: \(error)")
    }
}

checkEncoding()

Output

--- NSJSONSerialization Test ---
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Invalid type in JSON write (__SwiftValue)'
*** First throw call stack:
(
	0   CoreFoundation                      0x00000001804c97d4 __exceptionPreprocess + 172
	1   libobjc.A.dylib                     0x00000001800937cc objc_exception_throw + 72
	2   Foundation                          0x0000000180f83bb4 _writeJSONValue + 704
	3   Foundation                          0x0000000180f87b04 ___writeJSONObject_block_invoke + 408
	4   libswiftCore.dylib                  0x0000000195a3ff6c $ss26_SwiftDeferredNSDictionaryC23enumerateKeysAndObjects7options5usingySi_ys9UnmanagedVyyXlG_AHSpys5UInt8VGtXBtFTf4dnn_n + 796
	5   libswiftCore.dylib                  0x000000019584f824 $ss26_SwiftDeferredNSDictionaryC23enumerateKeysAndObjects7options5usingySi_ys9UnmanagedVyyXlG_AHSpys5UInt8VGtXBtFTo + 44
	6   Foundation                          0x0000000180f87090 _writeJSONObject + 436
	7   Foundation                          0x0000000180f838b4 -[_NSJSONWriter dataWithRootObject:options:] + 100
	8   Foundation                          0x0000000180f865e0 +[NSJSONSerialization dataWithJSONObject:options:error:] + 108

I found an old post from 2016 that mentions a similar issue that was patched

I'd like to understand if this is an issue with Swift, or if there is an alternative to NSJSONSerialization / JSONSerialization that would gracefully handle this error which I could recommend to the library maintainers.

Thanks!

JSONSerialization can't encode or decode native Swift types like structs or enums, which is one of the reasons it shouldn't be used. You can use JSONSerialization.isValidJSONObject to check whether your types can be encoded.

2 Likes

I'd make SimpleObject Encodable (and Decodable if needed) and use JSONEncoder (/ JSONDecoder).

let eventDict: [String: SimpleObject] = ["obj": obj]

or, if possibly:

struct EventDict: Codable {
    let obj: SimpleObject
}

and encode that.

That's good to know JSONSerialization will balk at Swift types, something I hadn't really considered. This is a 3p SDK, but I will pass along the feedback to them to investigate if JSONEncoder/Decoder would be more suitable.

It's unfortunate that even in a try/catch block, it still crashes the app.

Yes, you’ll want to check your object before you pass it, and the 3rd party should update to throw an error with that check.

If it helps, it’s considered a programmer error to not follow the contract of the method, rather than an unexpected run-time situation, and that’s why it’s not recoverable.

3 Likes

As a workaround consider catching that exception in Obj-C and converting it into an error, like so:

// <Your/Bridging/Header>.h
#import "NSJSONSerialization+Safe.h"
// NSJSONSerialization+Safe.h
#import <Foundation/Foundation.h>

@interface NSJSONSerialization (CatchingException)
+ (NSData *)safeDataWithJSONObject:(id)object options:(NSJSONWritingOptions)options error:(NSError**)error;
@end
// NSJSONSerialization+Safe.m
#import "NSJSONSerialization+Safe.h"

@implementation NSJSONSerialization (CatchingException)
+ (NSData *)safeDataWithJSONObject:(id)object options:(NSJSONWritingOptions)options error:(NSError**)error {
    @try {
        return [NSJSONSerialization dataWithJSONObject:object options:options error:error];
    }
    @catch(NSException* exception) {
        *error = [NSError errorWithDomain:@"NSJSONSerialization" code:-1 userInfo:@{NSLocalizedDescriptionKey: exception.description}];
        return nil;
    }
}
@end

Swift usage:

data = try JSONSerialization.safeData(withJSONObject: ...)

OTOH, does it give you much?

So instead of a trap you'll get an error - but that's during development time, right? And once you get that error you'd do something to fix the code anyway for that not to happen. So why bother?

As a workaround consider catching that exception in Obj-C and converting it into an error

You have to be careful with this. In general, it’s not safe to catch an Objective-C language exception and continue the execution of your app. That’s true in Objective-C [1], regardless of what’s going on with Swift.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

[1] In theory you can come up with specific situations where it’ll work reliably, but that’s not really viable in practice. Most notably, you have to eschew ARC O-:

1 Like

The documentation for JSONSerialization describes the supported types.
It might be helpful to include the following links in your feedback as well:

And with JSONEncoder encode is not throwing exceptions, or is it?

With JSONEncoder the requirement that every element be Codable is statically checked by the compiler, so this particular situation doesn’t come up. But if there are other violated preconditions they could very well decide they should fatalError about them rather than throwing.

There are a few fatal assertions in JSONEncoder, but they're all around internal encoder logic AFAICT. Thankfully the actual developer requirements are enforced by the compiler, unlike JSONSerialization's use of Any blobs.