NSJSON将普通的旧对象化?

NSJSONSerialization to plain old object?

提问人:Guss 提问时间:4/28/2022 最后编辑:Guss 更新时间:7/6/2022 访问量:232

问:

许多现代编程语言都有 JSON 库,这些库支持对 json 进行编码和解码,这些库支持将 json 编码到/从“普通旧对象”进行编码 - 即主要只具有数据属性的类的实例(属性可以是可以简单解码/编码的类型,也可以是其他普通的旧对象)。例子包括谷歌的GSON、golang的等。encoding/json

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

我知道可以枚举 Objective-C 类的属性,并且有人会使用该功能来创建 JSON“bean 映射器”似乎是合理的,但谷歌搜索对我来说没有产生任何结果,除了 Apple Swift 网站上的这篇博文展示了如何手动反序列化 JSON 到“模型对象”以及为什么他们认为自动执行此操作(DRYing 代码)是一个坏主意 (*)。

*) 原因基本上是,不需要编写大量样板(他们的示例实现是 36 LoC 来解析 3 个属性)并不是一个显着的改进,并且构建几个可选的回调来允许数据验证是很困难的。我显然不同意所有这些。

json objective-c json 反序列化 nsjsonserialization

评论

0赞 koen 4/28/2022
不确定这是否是你要问的,但你可以把你的json粘贴到这里:app.quicktype.io 并获取几乎任何你想要的语言的相应代码,包括Objective-C。
0赞 Guss 4/28/2022
不是我真正想要的 - 我正在寻找更像是“定义你的类然后调用库函数”而不是“生成完整的代码映射器”的东西,但看起来 Objective-C 的 QT 生成器基本上是包装和 - 我不知道,显然是 -> -> 的样板地狱所缺少的部分。所以这就是一个答案。NSObject::setValuesForKeysWithDictionaryNSObject::dictionaryWithValuesForKeysNSDataNSJSONSerializationsetValuesForKeysWithDictionary
0赞 Guss 4/28/2022
@koen,不过,我有点模糊,如果 JSON 定义了没有任何属性的字段会发生什么 - 在这种情况下会发生什么,但对调用者缺少字段或提供额外字段的弹性是必需的。setValue:forKey:
0赞 Guss 5/1/2022
Quicktype 的主要问题是生成的代码非常脆弱 - 它无法很好地处理可为 null 的值,并且在收到意外的 JSON 结构时会严重崩溃,而不是优雅地失败并在适当的位置保留默认值。

答:

1赞 Guss 4/30/2022 #1

这是我的解决方案,它不是基于库 - 因为我找不到任何库 - 而是使用 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];

欢迎评论家!(我不是很擅长目标C,可能做了很多失礼🤭)

问题:

  • 我可以处理可为空的数字(尽管 C 原语适用于不可为空的数字),但我不知道如何表示可为空的布尔值 - 即在使用 .NSNumber*withNullValues:NO
  • 发送没有属性的字段(例如,我使用的服务器同时发送蛇形大小写和 underscrore-case 的值以使其易于解析)会引发异常。(通过使用 和 代替 来解决)。respondsToSelector:setValue:setValuesForKeysWithDictionary:
  • 尝试将 nil 值设置为基元类型字段会导致异常。(通过检查属性类型和 解决)。NSNull
  • 完全不适用于嵌套对象 - 即具有也是自定义模型对象的属性的自定义模型对象。(通过检查属性类型和递归编码/解码来解决)。
  • 可能不能很好地处理数组 - 我的软件中还没有需要这些数组,所以我没有实现适当的支持(尽管我验证了编码简单的字符串数组效果很好)。