NSJSONSerialization 到普通的旧对象?

NSJSONSerialization to plain old object?

许多现代编程语言都有 JSON 支持编码和解码的库 json to/from“普通旧对象”——即主要只是 类 的实例具有数据属性(属性可以是普通类型 de/encoded 或其他普通的旧对象)。示例包括 Google 的 GSON,golang 的 encoding/json 和其他。

有没有类似Objective-C的东西?

我知道可以枚举 Objective-C 类 的属性,并且有人会使用该功能创建 JSON“bean 映射器”似乎是合理的,但是 Google 搜索对我没有产生任何结果,除了 this blog post on Apple's Swift website 展示了如何手动反序列化 JSON 为“模型对象”以及为什么他们认为自动执行此操作(DRYing 代码)是一个错误想法。

这是我的解决方案,它不是基于库——因为我找不到任何库——而是使用 Foundation 和 Objective-C 运行时方法——如上面的评论所述:

#import <objc/runtime.h>

NSArray<NSString*>* classPropertyList(id instance) {
    NSMutableArray* propList = [NSMutableArray array];
    unsigned int numProps = 0;
    objc_property_t* props = class_copyPropertyList(object_getClass(instance), &numProps);
    for (int i = 0; i < numProps; i++)
        [propList addObject:[NSString stringWithUTF8String:property_getName(props[i])]];
    free(props);
    return propList;
}

NSString* typeOfProperty(Class clazz, NSString* propertyName) {
    objc_property_t prop = class_getProperty(clazz, [propertyName UTF8String]);
    NSArray<NSString*>* propAttrs = [[NSString stringWithUTF8String:property_getAttributes(prop)] componentsSeparatedByString:@","];
    if ([(propAttrs[0]) hasPrefix:@"T@\""])
        return [propAttrs[0] componentsSeparatedByString:@"\""][1];
    return nil;
}

@implementation JSONMarshallable

- (NSData*)toJSON {
    return [self toJSON:self withNullValues:YES];
}

- (NSString*)toJSONString {
    return [self toJSONString:self withNullValues:YES];
}

- (NSData*)toJSON:_ withNullValues:(bool)nullables {
    NSError* error;
    NSDictionary* dic = [self toDictionary:self withNullValues:nullables];
    NSData* json = [NSJSONSerialization dataWithJSONObject:dic options:0 error:&error];
    if (!json) {
        NSLog(@"Error encoding DeviceConfigurationRequest: %@", error);
        return nil;
    }
    return json;
}

- (NSString*) toJSONString:_ withNullValues:(bool)nullables {
    NSData* json = [self toJSON:self withNullValues:nullables];
    return [[NSString alloc] initWithBytes:[json bytes] length:[json length] encoding:NSUTF8StringEncoding];
}

- (NSDictionary*)toDictionary:_ withNullValues:(bool)nullables {
    NSMutableDictionary* dic = [NSMutableDictionary new];
    for (id propName in classPropertyList(self)) {
        id val = [self valueForKey:propName];
        if (!nullables && (val == nil || val == NSNull.null))
            continue;
        if ([val respondsToSelector:@selector(toDictionary:withNullValues:)])
            val = [val toDictionary:val withNullValues:nullables];
        [dic setObject:(val == nil ? NSNull.null : val) forKey:propName];
    }
    return dic;
}

- (instancetype)initWithJSONString:(NSString*)json {
    return [self initWithJSON:[json dataUsingEncoding:NSUTF8StringEncoding]];
}

- (instancetype)initWithJSON:(NSData*)json {
    NSError* error;
    if (json == nil)
        return nil;
    NSDictionary* dataValues = [NSJSONSerialization JSONObjectWithData:json options:0 error:&error];
    if (!dataValues) {
        NSLog(@"Error parsing invalid JSON for %@: %@", NSStringFromClass(object_getClass(self)), error);
        return nil;
    }
    return [self initWithDictionary:dataValues];
}

- (instancetype)initWithDictionary:(NSDictionary*)dataValues {
    if (dataValues == nil)
        return nil;
    if (self = [super init])
        for (id key in dataValues) {
            id val = [dataValues objectForKey:key];
            if (![self respondsToSelector:NSSelectorFromString(key)])
                continue;
            NSString* typeName = typeOfProperty([self class], key);
            if ([val isKindOfClass:[NSNull class]]) { // translate NSNull values to something useful, if we can
                if (typeName == nil)
                    continue; // don't try to set nil to non-pointer fields
                val = nil;
            } else if ([val isKindOfClass:[NSDictionary class]] && typeName != nil)
                val = [[NSClassFromString(typeName) alloc] initWithDictionary:val];
            [self setValue:val forKey:key];
        }
    return self;
}

@end

然后通过从 JSONMarshallable 继承来创建自定义模型对象很容易,就像这样:

model.h:

#import "JSONMarshallable.h"

@interface MyModel : JSONMarshallable

@property NSString* stringValue;
@property NSNumber* numericValue;
@property bool boolValue;

@end

model.m:

@implementation MyModel
@end

SomeThingElse.m:

// ...

NSData* someJson;
MyModel* obj = [[MyModel alloc] initWithJSON:someJson];
NSString* jsonObj = [obj toJSONString:nil withNullValues:NO];

欢迎批评! (我不是很擅长Objective C,可能犯了很多失礼)

问题:

  • 我可以使用 NSNumber* 处理可为空的数字(尽管 C 基元可以很好地处理 non-nullable 数字),但我不知道如何表示可为空的布尔值 - 即一个可选的字段使用 withNullValues:NO.
  • 时未编码
  • 发送没有属性的字段(例如,我使用的服务器在 snake-case 和 underscrore-case 中发送值以便于解析)抛出exception.(通过使用 respondsToSelector:setValue: 而不是 setValuesForKeysWithDictionary: 解决)。
  • 尝试将 nil 值设置为 primitive-typed 字段会导致异常。(通过检查 属性 类型和 NSNull).
  • 对嵌套对象根本不起作用 - 即具有自定义模型对象属性的自定义模型对象。(通过检查 属性 类型解决并递归 encoding/decoding).
  • 可能不能很好地处理数组 - 我的软件中还不需要这些,所以我没有实现适当的支持(尽管我验证了编码简单字符串数组的效果很好)。