glideapps / quicktype

Generate types and converters from JSON, Schema, and GraphQL
https://app.quicktype.io
Apache License 2.0
12.29k stars 1.07k forks source link

Objective-C improvements #434

Open dvdsgl opened 6 years ago

dvdsgl commented 6 years ago

Reading about Mantle, a popular JSON framework for Objective-C, I spotted some good improvements for our Objective-C output:

dvdsgl commented 6 years ago

It seems that exceptions may be out of fashion in Objective-C as well...

dvdsgl commented 6 years ago

QTTopLevelFromData functions should catch exceptions and write to the NSError **error so there's only one style of error handling there.

n8chur commented 6 years ago

I noticed that you can annotate a property as nonnull by adding the property to the list of required fields in a schema. Is this nullability validated in any way?

Ideally it would be considered an error if one attempted to parse a model object with a JSON payload whose required property is missing or null.

n8chur commented 6 years ago

I agree that exceptions are "out of fashion". It's more common to pass a pointer to an NSError and optionally have it populated if the method in question returns NO or nil (depending on context).

n8chur commented 6 years ago

Defining an enum on a property of type string does not seem to generate an enum in the generated model (even when the appropriate checkbox is checked).

dvdsgl commented 6 years ago

@n8chur because of our recursive marshaling of dynamic values, I think threading the NSError in and out of our property initializers is going to be way too cumbersome. My plan is to still use exceptions internally but to catch them at the top level and convert them to NSError.

dvdsgl commented 6 years ago

@n8chur nonnull is validated in the NSDictionary objectForKey:withClass: method--this is also the source of the exceptions design choice.

dvdsgl commented 6 years ago

Our dynamic checking is far from comprehensive, though. For example, collections of boxed primitives (e.g. NSArray<NSNumber *> *) do not get their elements checked.

dvdsgl commented 6 years ago

@n8chur enum is still on our todo list (above), we just need to decide how to implement it. What would you expect/desire?

n8chur commented 6 years ago

@dvdsgl It would be great to have properties with typed enums generated something like:

typedef NS_ENUM(NSInteger, SomeEnum){
    SomeEnumValueA,
    SomeEnumValueB
};

Then the property might look like:

@property (readonly, nonatomic) SomeEnum enumValue;

All of the translation between NSString and the enum values could be under the hood and the strings themselves do not necessarily need to be exposed publicly.

dvdsgl commented 6 years ago

@n8chur excellent, that's exactly what we had in mind. I should get it in the next couple of days, but feel free to send a PR if you're feeling ambitious 😄 We could really use some passionate contributors with more specialized per-language knowledge.

dvdsgl commented 6 years ago

@n8chur alright, speedbump – what would you expect to happen when enums appear as values in JSON arrays and maps? Would you expect an NSArray<NSNumber *>? That seems a bit... tragic. Here's a sample and where we get into trouble.

n8chur commented 6 years ago

@dvdsgl I'm currently working on open sourcing a code/test fixture generation tool at Automatic but I may dive in after that's wrapped up!

n8chur commented 6 years ago

@dvdsgl Regarding enums: NSArray<NSNumber *> * seems reasonable, but perhaps with a comment about what that number is.

Another thing to consider is to use a box class for enum values when they need to be contained in NSArrays/NSDictionarys.

Something like:

@interface QTWeaknessBox : NSObject

@property (readonly, nonatomic) QTWeakness value;

@end

...

@property (nonatomic, readonly, copy) NSArray<QTWeaknessBox *> *weaknesses;
dvdsgl commented 6 years ago

@n8chur alright... Have a look: https://app.quicktype.io/#l=objc&s=pokedex

I had to make 'pseudo-enums' for reference semantics (boxing) and to preserve readable type names when enums become type parameters (e.g. I want NSArray<QTWeakness *> not NSArray<NSNumber *>):

@interface QTWeakness : NSObject
@property (nonatomic, readonly, copy) NSString *value;
+ (instancetype _Nullable)withValue:(NSString *)value;
+ (QTWeakness *)bug;
+ (QTWeakness *)dark;
+ (QTWeakness *)dragon;
+ (QTWeakness *)electric;
@end

@implementation QTWeakness
static NSMutableDictionary<NSString *, QTWeakness *> *qtWeaknesses;
@synthesize value;

+ (QTWeakness *)bug { return qtWeaknesses[@"Bug"]; }
+ (QTWeakness *)dark { return qtWeaknesses[@"Dark"]; }
+ (QTWeakness *)dragon { return qtWeaknesses[@"Dragon"]; }
+ (QTWeakness *)electric { return qtWeaknesses[@"Electric"]; }

+ (void)initialize
{
    NSArray<NSString *> *values = @[
        @"Bug",
        @"Dark",
        @"Dragon",
        @"Electric",
    ];
    qtWeaknesses = [NSMutableDictionary dictionaryWithCapacity:values.count];
    for (NSString *value in values) qtWeaknesses[value] = [[QTWeakness alloc] initWithValue:value];
}

+ (instancetype _Nullable)withValue:(NSString *)value { return qtWeaknesses[value]; }

- (instancetype)initWithValue:(NSString *)val
{
    if (self = [super init]) value = val;
    return self;
}

- (NSUInteger)hash { return value.hash; }
@end

Perhaps this is a tad unorthodox, but I think it's the best for now, on balance. It's also pretty nice for autocompletion and you can write expressions like pikachu.weakness == QTWeakness.water.

You can see in the code that I track the set of these pseudo-enums, which means they can be differentiated from natural NSUInteger enums. It makes sense to emit natural enums if they aren't used within generic types, so this leaves room for improvement. In the pokedex.json sample, QTPokemon.egg could be emitted as a natural enum, for example.