Skip to content

Instantly share code, notes, and snippets.

@mohiji
Created May 15, 2023 21:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mohiji/27623c84c787acc90b4e627cc21d5ccd to your computer and use it in GitHub Desktop.
Save mohiji/27623c84c787acc90b4e627cc21d5ccd to your computer and use it in GitHub Desktop.
Cheating to get predictable output out of NSJSONSerialization

I'm like one of three people still writing new Objective-C, but anyway: I've had customers request that I store data in a version-control friendly way, instead of a binary blob. My gut reaction to that is to use JSON: it's text, it diffs, etc. I want the diffs to be as friendly as possible though; if you change a single property in a document, I want to see a single line diff, and that means I'll need a predictable key order. Out of the box though, NSJSONSerialization only gives you two options: unordered keys, or lexicographic order. The latter would work, but I'd also like my keys to make some sort of sense.

It turns out that if you don't specifically ask for sorted keys, NSJSONSerialization will output them in the order provided by your NSDictionary's keyEnumerator. A little bit of subclassing (helped out by Cocoa with Love) and you get a stable output.

//
// JSONTest.m
//
#import <Foundation/Foundation.h>
@interface OrderedDictionary : NSDictionary
{
NSDictionary *_dictionary;
NSArray *_orderedKeys;
}
- (instancetype)initWithDictionary:(NSDictionary *)dict
keys:(NSArray *)keys;
@end
@implementation OrderedDictionary
- (instancetype)initWithDictionary:(NSDictionary *)dict
keys:(NSArray *)keys
{
self = [super init];
if (self != nil) {
_dictionary = [dict copy];
_orderedKeys = [keys copy];
}
return self;
}
- (NSUInteger)count
{
return [_dictionary count];
}
- (id)objectForKey:(id)key
{
return [_dictionary objectForKey:key];
}
- (NSEnumerator *)keyEnumerator
{
return [_orderedKeys objectEnumerator];
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSURL *desktopPath = [NSURL fileURLWithPath:[@"~/Desktop" stringByExpandingTildeInPath]];
NSDictionary *testDictionary = @{
@"name": @"Key Order Test",
@"formatVersion": @"1.0.0",
@"controllers": @[],
@"ioUnits": @[],
@"variables": @[]
};
NSError *err = nil;
NSData *data = [NSJSONSerialization dataWithJSONObject:testDictionary
options:NSJSONWritingPrettyPrinted
error:&err];
if (err != nil) {
NSLog(@"Failed to serialize JSON: %@", err);
}
else {
[data writeToURL:[desktopPath URLByAppendingPathComponent:@"testUnordered.json"] atomically:YES];
}
NSDictionary *orderedDictionary = [[OrderedDictionary alloc] initWithDictionary:testDictionary keys:@[@"name", @"formatVersion", @"controllers", @"ioUnits", @"variables"]];
data = [NSJSONSerialization dataWithJSONObject:orderedDictionary
options:NSJSONWritingPrettyPrinted
error:&err];
if (err != nil) {
NSLog(@"Failed to serialize JSON: %@", err);
}
else {
[data writeToURL:[desktopPath URLByAppendingPathComponent:@"testOrdered.json"] atomically:YES];
}
}
return 0;
}
{
"name" : "Key Order Test",
"formatVersion" : "1.0.0",
"controllers" : [
],
"ioUnits" : [
],
"variables" : [
]
}
{
"controllers" : [
],
"formatVersion" : "1.0.0",
"ioUnits" : [
],
"name" : "Key Order Test",
"variables" : [
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment