iPhone - Parse XML to custom objects

The following example is based on AmazonParser by Aaron Hillegass and the SeismicXML demo from Apple iPhone Dev Center.

Aaron's code works well in his example, but if the application needs to parse different XMLs, we may end up re-writing similar code over and over. To make Aaron's code more generic so it can be reused, I have made a few changes.

Let say that we want to process the contacts.xml and turn it into Contact objects on the client side.

<!-- Content of contacts.xml -->
<contacts>
   <Contact>
      <firstname>Joe</firstname>
      <lastname>Smith</lastname>
      <phone>321-123-4567</phone>
   </Contact>
   <!-- ... -->
</contacts>

// Contact.h
#import <UIKit/UIKit.h>

@interface Contact : NSObject {
   NSString *firstname;
   NSString *lastname;
   NSString *phone;
}

@property (nonatomic, retain) NSString *firstname;
@property (nonatomic, retain) NSString *lastname;
@property (nonatomic, retain) NSString *phone;

@end

// Contact.m
#import "Contact.h"

@implementation Contact

@synthesize firstname;
@synthesize lastname;
@synthesize phone;

@end

In the XMLToObjectParser, I have used NSObject to represent any class and to create an instance of that class at runtime using NSClassFromString method. Notice that I have made the tag names the same as the corresponding variable names to take advantage of key-value coding.

// XMLToObjectParser.h
#import <UIKit/UIKit.h>

@interface XMLToObjectParser : NSObject {
   NSString *className;
   NSMutableArray *items;
   NSObject *item; // stands for any class    NSString *currentNodeName;
   NSMutableString *currentNodeContent;
}
- (NSArray *)items;
- (id)parseXMLAtURL:(NSURL *)url
       toObject:(NSString *)aClassName
       parseError:(NSError **)error;
@end

// XMLToObjectParser.m
#import "XMLToObjectParser.h"

@implementation XMLToObjectParser

- (NSArray *)items
{
   return items;
}

- (id)parseXMLAtURL:(NSURL *)url
          toObject:(NSString *)aClassName
       parseError:(NSError **)error
{
   [items release];
   items = [[NSMutableArray alloc] init];
   
   className = aClassName;
   
   NSXMLParser *parser = [[NSXMLParser alloc] initWithContentsOfURL:url];
   [parser setDelegate:self];
   
   [parser parse];
   
   if([parser parserError] &amp;&amp; error) {
      *error = [parser parserError];
   }
   
   [parser release];
   
   return self;
}

- (void)parser:(NSXMLParser *)parser
didStartElement:(NSString *)elementName
namespaceURI:(NSString *)namespaceURI
qualifiedName:(NSString *)qName
   attributes:(NSDictionary *)attributeDict
{
   NSLog(@"Open tag: %@", elementName);
   
   if([elementName isEqualToString:className]) {
      // create an instance of a class on run-time       item = [[NSClassFromString(className) alloc] init];
   }
   else {
      currentNodeName = [elementName copy];
      currentNodeContent = [[NSMutableString alloc] init];
   }
}

- (void)parser:(NSXMLParser *)parser
didEndElement:(NSString *)elementName
namespaceURI:(NSString *)namespaceURI
qualifiedName:(NSString *)qName
{
   NSLog(@"Close tag: %@", elementName);
   
   if([elementName isEqualToString:className]) {
      [items addObject:item];
      
      [item release];
      item = nil;
   }
   else if([elementName isEqualToString:currentNodeName]) {
      // use key-value coding       [item setValue:currentNodeContent forKey:elementName];
      
      [currentNodeContent release];
      currentNodeContent = nil;
      
      [currentNodeName release];
      currentNodeName = nil;
   }
}

- (void)parser:(NSXMLParser *)parser
foundCharacters:(NSString *)string
{   
   [currentNodeContent appendString:string];
}

- (void)dealloc
{
   [items release];
   [super dealloc];
}

@end

To drive the parser, we type

#import "XMLToObjectParser.h"

NSURL *url = [NSURL URLWithString: @"http://localhost/contacts.xml"];
XMLToObjectParser *myParser = [[XMLToObjectParser alloc] parseXMLAtURL:url toObject:@"Contact" parseError:nil];

// do something with the contact array...

Some final notes, the sample code works fine with XML that has simple structure like that of contacts.xml. For complex structure like earthquake data http://earthquake.usgs.gov/eqcenter/catalogs/7day-M2.5.xml or if you need more control, you may be better off using TouchXML. Also, the code doesn't handle potential errors properly. I will leave that to you. :-)

Comments
Awesome. I had a little difficulty understanding SeissmicXML sample. But yours was great and i wrote a simple app already with that.

Thanks
# Posted By Mtuhu | 7/30/08 4:37 AM
Yo. (as a NooB to iPhone devl, I appreciate this approach)

Again - as a NooB to all of this, does this code stand alone or is it a add in to the mentioned Seismic XML code? Slightly confused.

Cheers GH
# Posted By Greg Huddleston | 8/13/08 3:55 AM
Bless you, good sir. You just saved me from having to write exactly this myself.
# Posted By Ryan | 8/15/08 1:57 PM
Hi Greg,

Yes, the sample code is independent of the SeismicXML demo. In fact, simply copy XMLToObjectParser.h and XMLToObjectParser.m to your project and you are ready to go. The last code snip shows a sample usage.

Andrew
# Posted By Andrew | 8/18/08 5:47 AM
if i see <contact name="John Doe">, how does it handle attribleDict in didStartElements?
# Posted By Jonathan | 8/23/08 2:05 AM
Maybe a VERY stupid question, but how do i get the myParser back to a Contact object ?
# Posted By Kåre Knall | 8/25/08 6:26 AM
Thank you for a VERY good tutorial.

I have a question that I cant seem to figoure out myself.
What if Joe Smith has several phone numbers ?
Now only the last number will be added to the object :(
# Posted By Jimmy Bergetun | 8/25/08 8:58 PM
Hi Jonathan,

You can handle attributeDict using a for loop:

for (id key in attributeDict)
NSLog(@"key: %@, value: %@", key, [attributeDict objectForKey:key]);

So, for <contact name="John Doe">, you should see the following in the console

Open tag: contact
key: name, value: John Doe

Andrew
# Posted By Andrew | 8/27/08 5:26 AM
Hi Kåre,

To get back Contact objects, do

   for(int i = 0; i < [[myParser items] count]; i++)
      NSLog(@"item: %@", ((Contact *)[[myParser items] objectAtIndex:i]));

Andrew
# Posted By Andrew | 8/27/08 5:37 AM
Hi Jimmy,

You need to modify the Contact object a bit to handle multiple phone numbers. Instead of using NSString to store a phone number, use a NSMutableArray object. In the setter function for phone, simply add the given phone number to the array.

Modified Contact

//Contact.h
#import <UIKit/UIKit.h>

@interface Contact : NSObject {
   NSString *firstname;
   NSString *lastname;
   NSMutableArray *phone;
}

@property (nonatomic, retain) NSString *firstname;
@property (nonatomic, retain) NSString *lastname;

- (void)setPhone:(NSString *)value;
- (NSMutableArray *)phone;

@end

//Contact.m
#import "Contact.h"
@implementation Contact

@synthesize firstname;
@synthesize lastname;

- (void)init {
   [super init];
   phone = [[NSMutableArray alloc] init];
}

- (void)setPhone:(NSString *)x {
   [phone addObject:x];
}

- (NSMutableArray *)phone {
   return phone;
}

@end

Andrew
# Posted By Andrew | 8/27/08 6:14 AM
if i have an xml such as
<kingtag>
<queen>teej</queen>
<slave>mootal</slave>
<parent>
<desk>
<pencil>kavs</pencil>
<rubber>nataraj</rubber>
</desk>
<desk>
<pencil>kavs1</pencil>
<rubber>nataraj1</rubber>
</desk>
</parent>
</kingtag>
how do i go about parsing it
# Posted By Gaurav Srivastava | 9/7/08 7:00 PM
Great post. Just found this tonight and now am able to load local xml file from my phone. This saved me so much time and was much easier to follow than the seismic demo.
# Posted By Chris | 10/23/08 4:57 PM
Hi, nice topic!

is it possible to do a check if the key exists?
and if not .. he just skips it because now i get an error

i have a webservice XML i want to put in an object but just certain parts of the xml not the whole XML.
# Posted By Andy Jacobs | 11/17/08 6:16 PM
2008 Atrexis Systems Limited | EMail