How I Tackled Parsing XML on Android When JAXB Just Wouldn’t Play Nice

How I Tackled Parsing XML on Android When JAXB Just Wouldn’t Play Nice

If you’ve ever tried using JAXB (Java Architecture for XML Binding) on Android, you probably know that it doesn’t exactly play well with the platform. I ran into this issue recently while working with a shared library that required JAXB for XML parsing. Unfortunately, I quickly found out that JAXB was more finicky than I had hoped, and it just wouldn’t work on Android as expected.

After a fair amount of trial and error, I decided to switch gears and use Android’s native XmlPullParser to manually handle the parsing. But that decision led to its own set of challenges—getting everything to work smoothly and testing it out wasn’t as straightforward as I thought it would be. All of this was necessary because I was working with a cross-platform library, which made the situation a bit more complicated.

In this article, I’ll walk you through the steps I took, the code I wrote, and the struggles I faced along the way. I’ll also point out areas where I think the code could be further optimized and offer suggestions for improvement.

The Frustration of JAXB and the Road to Manual Parsing

When I first set out to parse XML data in Android, I thought using JAXB would be a smooth solution. I quickly learned it wasn’t — JAXB just didn’t work well with Android. As much as I hoped to use it to automatically bind XML data to Java objects, it didn’t cooperate, which led me to fall back on manual parsing using Android’s XmlPullParser.

The real challenge started with setting up the parser, reading from the input stream, and processing the XML tags. What I was left with was a basic XML parsing loop that checked the tags and mapped them to fields of the desired class instance. Here’s the approach I took for this part:

If the embedded code does not load, open it on GitHub Gist.

Why this hurt

At this point, I was simply setting up the parser, reading the XML, and mapping each tag to the class. The real pain came later when I had to handle map fields and manually map the values to class fields. But here, it was still relatively straightforward: check the tags, read the text, and set the values. The frustration kicked in when I couldn’t fully utilize JAXB’s automatic binding capabilities, and that’s when the real pain started to show up.

Annotations & Reflection & Default Values, Oh My!

When parsing XML manually like this, the biggest frustration — beyond the general complexity — was using reflection to access and set private fields. Here’s the core of the process:

If the embedded code does not load, open it on GitHub Gist.

In this method, I’m checking whether the field name matches the tagName from the XML. If it does, I use reflection to access the private field and set its value. Normally, JAXB or similar libraries handle this behind the scenes, but in this case, I had to do it manually.

Reflection Headache

The biggest pain here was using reflection to access fields that were marked as private. Reflection is powerful but incredibly tricky to get right when you’re dealing with types and values that don’t always match up as expected. It's also fragile because it bypasses compile-time checks. This is something I really felt the sting of when dealing with data types and field names that didn’t quite line up.

Dealing with ##default Values

##default

One of the more head-scratching problems was dealing with the annotation value ##default—which is generated by JAXB for fields that have default values in the schema. The catch? The parser would pick up ##default and treat it as an actual value, which was far from ideal:

If the embedded code does not load, open it on GitHub Gist.

The getFieldName() method checks whether the annotation's name is empty or set to ##default. If it’s set to ##default, it skips it, otherwise it uses the name provided in the annotation. This was a necessary workaround since ##default would have otherwise made my reflection process much more complicated than it needed to be.

Lesson learned

If you’re working with JAXB in Android (or in any project with manual parsing), watch out for ##default values—it can cause unnecessary headaches and wasted time. In this case, I had to ensure these default values didn’t get mistaken for real data, otherwise the class fields would have been incorrectly populated.

Oh look, a Map

Before Map Handling

This is the initial version where there was no map handling logic. The focus was solely on parsing regular fields and populating them in the instance:

If the embedded code does not load, open it on GitHub Gist.

Explanation:

  • The code simply reads through the XML file, parses each tag, and assigns the parsed values to fields in the instance using setField().
  • No map-specific logic is involved here; it assumes all fields are simple types and can be directly set from XML.

After Map Handling

The updated version incorporates logic for parsing map fields (key-value pairs) by detecting map fields in the class, storing key-value pairs, and using a more complex approach for handling the map data.

If the embedded code does not load, open it on GitHub Gist.

Explanation:

  • Detect Map Fields: The isMapField() method is used to detect if a tag corresponds to a map field in the class. If it does, currentMapField is set.
  • Handle Key-Value Pairs: In the TEXT event, if currentKey is set, it means we are inside a map and need to handle key-value pairs. The handleMapKeyValueEntry() method processes the key and value, ensuring they are added to the map properly.
  • Reset Keys After Parsing: Once the map key-value pair is processed, currentKey and currentMapField are reset when the END_TAG for the respective key and map fields is encountered.

Map-Specific Logic Explained

Here’s a breakdown of the key changes made to handle maps, starting with the handleMapKeyValueEntry method:

If the embedded code does not load, open it on GitHub Gist.

  • Key-Value Handling: This method checks if we’ve already captured a key or not. If the key is still empty, it stores the current value as the key. Once both the key and the value are ready, it calls addToMap() to store them in the appropriate map.

Identifying Map Fields

The isMapField() method checks if a given field is a map. If the field is a map, it returns true, indicating that special handling is required for the field.

If the embedded code does not load, open it on GitHub Gist.

  • Map Detection: The method loops through all declared fields and checks if any field is a Map. It compares the tagName from the XML with the field name to decide if that field should be treated as a map.

Adding Key-Value Pairs to the Map

Once we detect a map field, the addToMap() method is used to add the parsed key-value pair to the map:

If the embedded code does not load, open it on GitHub Gist.

  • Map Population: This method uses reflection to access the map field. It ensures that the field is accessible and that the map is initialized. Then, it adds the parsed key-value pair to the map using the correct data type for the value.

Pain Points with Maps

1. Complexity of Reflection with Maps: Dealing with maps required additional reflection, which is error-prone and led to more complexity. Each time we encountered a map field, we had to ensure that the key-value pairs were added correctly, without messing up field access.

2. Key-Value Parsing: Another pain point was the handling of key-value pairs — managing two separate pieces of data (key and value) while parsing was tricky. Especially with mixed types in XML, ensuring proper mapping was difficult and error-prone.

3. Handling Null and Empty Values: Checking for null or empty values before adding them to the map was an extra safeguard to avoid unwanted behavior or exceptions. But getting this right took time and required additional testing.

The Joys of Writing Tests

Once the tests for the XML parsing functionality were written, it became clear that XmlPullParser wasn’t working in the JUnit testing environment. The issue? XmlPullParser relies on Android-specific components and requires an Android context, meaning it only works in an instrumented test (one that runs on a real or virtual device, not in a JUnit test on the JVM).

This posed a significant challenge because the module being tested was encapsulated as a service with no UI — so switching to instrumented tests wasn’t a simple option. After struggling with this limitation and trying to adjust the setup to run instrumented tests (which didn’t work for various other reasons), I found myself walking away from the code multiple times for a breather and more research.

In the end, after a bit of desperation and a couple of walks to clear my head, I stumbled upon the solution: KXML2.

Solution: Adding KXML2

To resolve this issue, I added the following dependency to the project:

testImplementation(libs.kxml2)
kxml2 = "2.3.0"
kxml2 = { module = "net.sf.kxml:kxml2", version.ref = "kxml2" }

This brought in KXML2, a lightweight XML parser that works seamlessly in both Android and Java environments.

Why KXML2 Was Required

  • XmlPullParser and the Need for Context: As mentioned, XmlPullParser from Android requires an Android context to function properly. When trying to run JUnit tests, the test environment doesn’t have the necessary context to work with the Android-based parser. Instrumented tests, which are designed to run on Android devices, are required to handle the context, but this wasn’t feasible in our case due to the nature of the module and other issues with setting up the instrumented tests.
  • KXML2 as a Standalone Solution: KXML2 doesn’t require an Android context and can be used directly in JUnit tests on the JVM. This made it the perfect solution to run XML parsing tests in a pure Java environment. The library supports pull-based parsing just like XmlPullParser, but it works independently of Android-specific dependencies, which allowed the tests to be executed in JUnit.
  • Cross-Platform Compatibility: KXML2 ensures compatibility with both Android and Java, making it a great tool for cross-platform projects like this one. It’s lightweight and does exactly what’s needed for our XML parsing without the overhead of Android-specific context requirements.

Switching to KXML2 solved the issues with XmlPullParser in JUnit tests. It allowed us to run the tests in a standard Java environment without relying on an Android context, making it a perfect fit for unit testing XML parsing functionality. After some frustration and trial and error, KXML2 emerged as the hero to save the day and restore sanity.

Conclusion

In the end, the journey from writing the code to overcoming the testing hurdles proved to be both challenging and enlightening. The initial assumption that XmlPullParser would work seamlessly in JUnit tests was quickly shattered when we realized it required an Android context—something that wasn't available in a pure JUnit setup. This led to frustration, multiple attempts to switch to instrumented tests, and several walks away from the code to clear my head.

However, through some research, persistence, and a bit of luck, the solution came in the form of KXML2. This lightweight XML parser allowed us to bypass the need for an Android context and provided a straightforward way to run XML parsing tests on the JVM. By switching to KXML2, we could seamlessly run our unit tests without the overhead and complexity of Android-specific dependencies.

This experience reinforced a valuable lesson: even when things don’t go as planned, sometimes the solution is just a bit of research away. With KXML2, we found a reliable solution to our testing issues, allowing us to continue improving our XML parsing functionality while keeping the tests fast and efficient.

The struggle was worth it, and now we have a more robust setup for testing XML-related features in a non-UI service module, ensuring that our application behaves as expected in a variety of environments.