YAML to Objective-C Converter

Paste a config file, read back a .h and .m pair. Each nested mapping turns into a prefixed NSObject subclass with initWithDictionary: and dictionaryRepresentation, typed from the values in your file, with keys like id and description renamed before the compiler complains.

YAML to Objective-C generator

config.yaml 0 lines
Build settings
Header
Properties
Round trip
Waiting for YAML
0 classes

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:

objc[4127]: Class Config is implemented in both .../Frameworks/Analytics.framework/Analytics and .../Toolexe Reader.app/Toolexe Reader. One of the two will be used. Which one is undefined.

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:

RouteWhat reads the file at runtimeWhat happens to nullFits when
YAML to JSON at build timeNSJSONSerializationArrives 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 timeNSDictionary 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 appA C library plus a wrapper you maintainWhatever the wrapper decidesThe 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.

copyNSString and NSArray. Both have mutable subclasses. Without copy, a caller passing an NSMutableString keeps the power to change your model after the fact.
strongNSNumber, the generated model classes, and id. NSNumber has no mutable subclass, and a nested model is owned, not duplicated.
assignNSInteger, double and BOOL. Plain C values, no object and no reference count involved.

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.

YAML in
id: rc-2026-09
description: Default remote config
new-release-notes: Faster sync
Header out
@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:

error: property follows Cocoa naming convention for returning 'owned' objects

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:

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.

Questions about YAML to Objective-C

Xcode setup, Swift interop, missing keys at runtime, and why some names changed.

How do I add the generated files to an Xcode project?

Download both the .h and the .m, drag them into the Xcode project navigator, and tick your app target in the dialog Xcode shows. The .m then appears under Build Phases, Compile Sources. Import the header wherever you read the config, for example #import "TXEAppConfig.h", and build one object with [[TXEAppConfig alloc] initWithDictionary:dictionary], where dictionary is the result of NSJSONSerialization or a plist read.

Do these classes work from Swift?

Yes, through the bridging header. Add #import "TXEAppConfig.h" to YourApp-Bridging-Header.h and the classes show up in Swift. Keep "Nullability annotations" ticked, or every object property imports as an implicitly unwrapped optional. Note the renamed getter: a property with getter=theNewReleaseNotes is still reached as config.newReleaseNotes from Swift, since Swift uses the property name.

What does a model hold when a key is missing at runtime?

Required fields fall back to an empty value instead of crashing: an empty string, an empty array, 0 for numbers and NO for BOOL, and a nested model built from an empty dictionary. Fields the YAML showed as optional stay nil. The trade-off is a config with a typo in a key name loads without complaint and carries defaults, so check the values you depend on once at startup.

Why does my class prefix turn into capitals?

Class prefixes are uppercase by convention, NS, UI, and the three-letter prefixes app teams pick. The field strips anything besides letters and digits and uppercases the rest, and a prefix starting with a digit is dropped because no Objective-C identifier opens with one. The same prefix also names the static ReadValue helper near the top of the .m, TXEReadValue in the sample.

Why is the class for my api key called TXEAPI and not TXEApi?

Cocoa keeps initialisms in capitals, as in NSURL, NSURLSession and the baseURL property on NSURL. The generator does the same for common ones, URL, ID, API, HTTP, JSON, UUID, OS and a few more, so base-url becomes baseURL and user-id becomes userID. Anything outside that list gets ordinary camel case.

Does the generator handle YAML anchors and merge keys?

Yes. Anchors like &defaults, aliases like *defaults and merge keys written <<: *defaults all resolve before typing, so a block merging shared settings produces a class with every inherited key plus its own. The anchor block itself also gets a class if it sits at a normal key, since it is still a mapping in the file.

Is my YAML sent to a server?

No. The YAML parser and the code generator both run as JavaScript inside this page after it loads. Nothing is uploaded, logged or stored, so a config with internal hosts, feature flags or rollout numbers stays on your machine. Closing the tab clears both panes.