XPath Query Returns No Results Using Libxml2 and GDataXmlDocument nodesForXPath

If you are using TouchXML or GDataXML for reading XML documents, you may have run into a problem with your XPath queries. I was working with the Freshbooks API to return a list of projects and it returned some fairly simple XML (I have abbreviated all the attributes of a project element for clarity):

<?xml version="1.0" encoding="utf-8"?>
<response xmlns="http://www.freshbooks.com/api/" status="ok">
  <projects page="1" per_page="15" pages="1" total="5">
    <project>
      <name>Super Fun Project</name>
    </project>
  </projects>
</response>

So now I wanted to simply retrieve all the project elements in the response using XPath. Seemed simple enough, so this is what I wrote:

// doc is an instance of GDataXMLDocument
NSArray *projects = [doc nodesForXPath:@"//projects/project" error:nil];
NSLog(@"%d", projects.count);

My NSLog statement always returned 0 results. This started to drive me nuts. I tried several different XPath queries and still no luck. There were no errors even when I used the error outlet, and I could even loop through the elements manually and see that there were indeed project elements being returned. So what’s the problem?

The problem had to do with namespaces. The XPath query needed to be written using the correct XML namespace, so something like this:

// doc is an instance of GDataXMLDocument
NSArray *projects = [doc nodesForXPath:@"//ns:projects/ns:project" error:nil];
NSLog(@"%d", projects.count);

Great, but what is the namespace name? According to my XML response it was http://www.freshbooks.com/api/. However the GDataXMLDocument knew nothing about it.

I started digging into the source of GDataXML and came across this in the nodesForXPath method:

int result = xmlXPathRegisterNs(xpathCtx, prefix, nsPtr->href);
if (result != 0) {
#if DEBUG
    NSCAssert1(result == 0, @"GDataXMLNode XPath namespace %@ issue",prefix);
#endif
}

Ok… some simple debugging showed it was actually registering a namespace, but what was it called? That lead me to some code just before the xmlRegisterNs call:

if (prefix == NULL) {
    prefix = (xmlChar*) kGDataXMLXPathDefaultNamespacePrefix;
}

Now we’re getting somewhere. Next stop the definition of the kGDataXMLXPathDefaultNamespacePrefix constant:

// when no namespace dictionary is supplied for XPath, the default namespace
// for the evaluated tree is registered with the prefix _def_ns
_EXTERN const char* kGDataXMLXPathDefaultNamespacePrefix _INITIALIZE_AS("_def_ns");

THAT WOULD HAVE BEEN HELPFUL AN HOUR AGO… Anyway, I went ahead and changed my original XPath query to:

// doc is an instance of GDataXMLDocument
NSArray *projects = [doc nodesForXPath:@"//_def_ns:projects/_def_ns:project" error:nil];
NSLog(@"%d", projects.count);

BOOM. Now we’re cooking with gas. It correctly returned all my project elements. So the lesson learned here is that GDataXML and TouchXML will not register your namespace or use it by default. You have to use the default, or register it manually.

How do you register a namespace with GDataXML manually you ask?

// doc is an instance of GDataXMLDocument
NSDictionary *ns = [NSDictionary dictionaryWithObjectsAndKeys:@"http://www.freshbooks.com/api/", @"fb", nil];
NSArray *projects = [doc nodesForXPath:@"//fb:projects/fb:project" namespaces:ns error:nil];
NSLog(@"%d", projects.count);

And finally, if you want to programatically register a namespace using GDataXml, then you would populate your NSDictionary by iterating through the namespaces found by GDataXMLElement:

// doc is an instance of GDataXMLDocument
NSArray *namespaceURIs = [doc.rootElement namespaces];

Hopefully that saves you some time and sanity.