Two files, every class inside them
The generator writes one header and one implementation file, no matter how many mappings your YAML holds. The sample above turns into three classes, TXEAppConfig, TXEAPI and TXEFeatureFlag, and all three live in TXEAppConfig.h and TXEAppConfig.m. Drag both files into Xcode, tick your app target in the dialog, and the models build.
Splitting each class into its own pair later is a cut and paste job. The @class line near the top of the header already lists every forward declaration a split needs, so no class has to import another header to name a property type.
Why every class name opens with three capitals
Objective-C has no namespaces. Every class in your app, in every framework you link and every pod you install, shares one flat list of names at runtime. Ship two classes called Config and the runtime prints this at launch and loads only one of them:
Apple keeps two-letter prefixes such as NS, UI and CA for its own frameworks, so app code uses three. TXE stands for Toolexe in the sample. Type your own into Class prefix, or clear the field for a target where the models never leave the app and never meet a third-party SDK.
Foundation has no YAML reader, so decide where parsing happens
Nothing in Foundation, UIKit or AppKit parses YAML. The generated classes take an NSDictionary, which means something else has to turn the file into one first. Three routes show up in real projects:
| Route | What reads the file at runtime | What happens to null | Fits when |
|---|---|---|---|
| YAML to JSON at build time | NSJSONSerialization | Arrives as NSNull. The generated helper turns it back into nil. | Config bundled with the app, or served from your backend as JSON |
| YAML to plist at build time | NSDictionary dictionaryWithContentsOfURL:error: | Plists have no null type, so the key is dropped. initWithDictionary: treats a missing key and a null key the same way. | Small settings files, with no JSON step at all |
| libyaml inside the app | A C library plus a wrapper you maintain | Whatever the wrapper decides | The app reads or edits YAML files on the device, which is rare |
For a config file bundled with the app, convert at build time. A Run Script phase placed before Copy Bundle Resources keeps YAML in the repository and JSON in the app bundle, and the classes above read the result without a single change. Pulling a YAML parser into the binary only pays off when the device itself has to read YAML nobody converted ahead of time.
copy, strong, assign: why the attributes differ line by line
Each property gets the ownership attribute Cocoa convention expects for its type, not one blanket choice for the whole class.
copy, a caller passing an NSMutableString keeps the power to change your model after the fact.id. NSNumber has no mutable subclass, and a nested model is owned, not duplicated.With Read-only ticked, the header also marks init and new as NS_UNAVAILABLE and flags initWithDictionary: as the designated initializer. The compiler then rejects [[TXEAppConfig alloc] init], so no half-filled config object ever exists. Untick it for readwrite properties and a plain init, useful when a test builds models by hand.
Inside initWithDictionary: the values go straight into the instance variables, _appName = ..., never through self.appName. Apple's memory management guide says to skip accessor methods in initializers, since a subclass overriding a setter would run against an object still under construction.
NS_ASSUME_NONNULL and what Swift sees
Nullability annotations matter most in a mixed codebase. Leave them off and Swift imports every object property as an implicitly unwrapped optional, String!, which crashes on nil with no warning at compile time. With them on, appName imports as String and rollout as NSNumber?, which matches what the YAML showed. A property only turns nullable when your file gives a reason: a ~ or null value, or a key missing from some entries of a list.
Keys the compiler refuses
Three keys in the sample would break the build if they kept their obvious names.
id: rc-2026-09
description: Default remote config
new-release-notes: Faster sync
@property (nonatomic, copy, readonly) NSString *identifier;
@property (nonatomic, copy, readonly) NSString *descriptionValue;
@property (nonatomic, copy, readonly,
getter=theNewReleaseNotes) NSString *newReleaseNotes;
id is the Objective-C type for any object, so a property with the same name reads badly at every call site. identifier is the name Cocoa itself uses. description is worse, because it compiles and then quietly overrides -[NSObject description]. NSLog(@"%@", config) would print the release description instead of the object, and every debugger po on the model would lie to you.
new-release-notes trips a rule most people forget exists. Under ARC, a method whose name opens with alloc, copy, mutableCopy, new or init returns an object the caller owns. A getter named newReleaseNotes makes the same promise, and clang stops the build:
The getter= attribute renames the method and leaves the property name, the dot syntax and the _newReleaseNotes instance variable alone. The rule only fires when the prefix is followed by a capital letter or nothing, so a key like newsletter-url passes untouched. In every case the dictionary lookup keeps the original key, dictionary[@"new-release-notes"], so your YAML file never has to change.
Why is rollout an NSNumber when I picked scalars?
Because one of the three feature flags leaves rollout out. A double has no empty state, and 0 would read as "rolled out to nobody", a different instruction entirely from "not set". A nil NSNumber keeps the two apart, so any field the YAML shows as missing or null stays boxed, even with scalars selected. The generator notes list names each field it boxed this way.
Numbers carry one more trap. rollout: 1.0 reaches the generator as the whole number 1, since the YAML parser in your browser keeps no record of the decimal point. The field turns double only because a sibling entry holds 0.25. Paste a file where every sample value is whole, and the field types as NSInteger. integerValue then truncates a later 0.5 to 0 without a word. Write one fractional value in the sample, or switch Numbers to NSNumber for everything.
This page reads YAML 1.2, where only true and false are booleans. enabled: yes types as NSString here. Plenty of build scripts still read YAML 1.1, where yes, on and off all mean true or false. Convert with one of those and the JSON inside your app holds a boolean while the header says string. Write true and false in the source file and both readers agree.
NSNull, and the helper at the top of the .m
NSJSONSerialization turns every JSON null into [NSNull null], a real object, not nil. Send it integerValue and the app dies with -[NSNull integerValue]: unrecognized selector sent to instance. The static TXEReadValue function at the top of the implementation file returns nil for NSNull, and nil for a value of the wrong class, a string sitting where a number belongs.
Messaging nil returns 0, so a scalar read of bad data yields 0 instead of a crash. The trade-off is silence. For a flag where a wrong value quietly flips app behavior, add an NSLog to the last branch of the helper so type mismatches show up in the console during testing.
Where this generator stops
Anchors, aliases and <<: merge keys all resolve before typing, so a block built from a shared default turns into a class with every inherited key. Block scalars written with | or > come through as ordinary strings. The rest has edges:
- One sample, one guess. Types come from the values you paste. A key absent from the sample is absent from the class, and
initWithDictionary:ignores it at runtime. - Array contents go unchecked.
supportEmailsis declaredNSArray<NSString *>, but lightweight generics only exist at compile time. A number slipped into the list at runtime stays a number, and the copy passes it straight through. - Lists of lists stay raw. A nested list types as
NSArray<NSArray *>with no class built for its entries. - Timestamps stay strings. An unquoted
2026-09-01types asNSString. Parse it withNSISO8601DateFormatterwhere you need anNSDate. - No NSSecureCoding, NSCopying, isEqual: or hash. Add them by hand if the models get archived to disk or used as dictionary keys.
- Integers above 9,007,199,254,740,992 lose digits in the browser parser before any type is picked. Quote large IDs in the YAML so they arrive as strings.
- First document only. A file split with
---gets its first document typed and the rest listed in the notes. - ARC only. The implementation assigns objects to instance variables without a retain. In a manual reference counting target, add
-fobjc-arcto these two files under Build Phases, Compile Sources. - Nothing leaves the page. Parsing and code generation both run in this browser tab. A config holding an API host or a feature rollout plan is never uploaded.
Starting a new app in Swift? A Codable struct replaces all of this in a dozen lines, and this output is the wrong starting point. The generator earns its place in Objective-C codebases and mixed targets, where the models have to be visible from both languages.
For an API response rather than a config file, the JSON to Objective-C converter reads the payload directly. XML feeds go through the XML to Objective-C converter. And a YAML file refusing to parse here deserves a pass through the YAML validator, which lists every problem at once.