Reading XML in Java With JAXB Annotations and Its Defaults
Map XML to Java classes with Jakarta XML Binding, read attributes, lists and optional elements, and see a bad boolean and a bad number accepted silently.

If the XML is the shape of your data, binding is the least code. You describe classes with annotations, hand the file to an unmarshaller and get objects back, with typed fields and without writing a loop. In Java that technology is JAXB, now maintained as Jakarta XML Binding. It is convenient and it has a default that surprises people: it accepts bad data without complaint.
Dependencies
JAXB used to ship with the JDK. Since Java 11 it does not, and the package name moved from javax.xml.bind to jakarta.xml.bind with Jakarta EE 9. A project needs the API and an implementation:
<dependency><groupId>jakarta.xml.bind</groupId><artifactId>jakarta.xml.bind-api</artifactId><version>4.0.2</version></dependency>
<dependency><groupId>org.glassfish.jaxb</groupId><artifactId>jaxb-runtime</artifactId><version>4.0.5</version></dependency>Code copied from older answers uses javax.xml.bind and fails to compile against the new packages; the annotations are the same, only the package name changes. The resolved class path for my test was seven jars.
The classes
@XmlRootElement(name = "library")
@XmlAccessorType(XmlAccessType.FIELD)
public static class Lib {
@XmlAttribute public String updated;
@XmlElement(name = "book") public List<Book> books = new ArrayList<>();
}
@XmlAccessorType(XmlAccessType.FIELD)
public static class Book {
@XmlAttribute public String id;
@XmlAttribute public boolean available;
public String title;
@XmlElement(name = "author") public List<String> authors = new ArrayList<>();
public Price price;
public String note;
}
@XmlAccessorType(XmlAccessType.FIELD)
public static class Price {
@XmlAttribute public String currency;
@XmlValue public BigDecimal amount;
}The mapping is declarative. @XmlAttribute binds an XML attribute to a field. A field with no annotation becomes a child element of the same name (with XmlAccessType.FIELD, which I prefer to the default of public properties because it does not depend on getters). @XmlElement(name = "author") on a list says that each repeated author element is one entry, so a book with one author and a book with two both get a list, which is the shape problem that generic converters fail at. @XmlValue on amount binds the element's text, and makes Price a class with an attribute and a value, the pattern for <price currency="USD">29.50</price>.
Reading
var ctx = JAXBContext.newInstance(Lib.class);
Lib lib = (Lib) ctx.createUnmarshaller().unmarshal(new File("library.xml"));b1 | Cache Patterns | [Ana Lee, Bo Chen] | 29.50 USD | available=true | note=Covers R&D
b2 | SQL Tuning, 2nd Edition | [Cy Ortiz] | 44.00 USD | available=false | note=null
b3 | Indexes "Explained" | [Di Park] | 250000 VND | available=true | note=
USD total 73.50Three typed objects. The authors are lists, the boolean attribute is a real boolean, and the price is a BigDecimal that kept its trailing zero (29.50), so the USD total of 73.50 is exact. The note shows the absent and empty cases apart: null for book 2, which had no note element, and the empty string for book 3, which had <note/>. The ampersand arrived decoded. Writing the objects back out is the mirror:
var m = ctx.createMarshaller();
m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
m.marshal(lib, writer);<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<library updated="2026-10-01">
<book id="b1" available="true">
<title>Cache Patterns</title>
<author>Ana Lee</author>
<author>Bo Chen</author>
<price currency="USD">29.50</price>
<note>Covers R&D</note>
</book>It reproduced the structure with a four-space indent and escaped the ampersand again. Round-tripping works, with the usual caveats: comments are gone, element order follows the class and attribute order may differ from the input.
The silent default
Here is the part that surprises people. I fed it a document with a boolean that is not a boolean and a price with a decimal comma:
<library><book id="x" available="maybe"><price currency="USD">9,5</price></book></library>With the unmarshaller as created, the call returned normally. There was no exception, and the object had available = false and amount = null. The default behavior of JAXB is to treat conversion problems as recoverable events, ignore them and carry on, so a typo in a feed becomes a missing value in your objects, and nothing logs it. Registering an event handler turns the events into something you can see:
var u = ctx.createUnmarshaller();
var errs = new ArrayList<String>();
u.setEventHandler(ev -> { errs.add(ev.getMessage()); return true; });
Lib bad = (Lib) u.unmarshal(new StringReader(xml));with handler: [Character , is neither a decimal digit number, decimal point, nor "e" notation exponential mark.] | amount=null | available=falseThe handler received one message, for the comma in 9,5. (The maybe boolean produced no event at all in this version; it silently became false.) Returning true from the handler continues the parse; returning false aborts it with an exception. For a loader that must not accept bad data, return false for every event, or collect the messages and fail at the end with the full list. This is the single most important JAXB setting, and it is not on by default. A stricter route is to validate against a schema first (setSchema) so structural and type errors fail before binding; see validating XML against an XSD schema. Also remember that a boolean in a schema-validated document is true, false, 1 or 0, and maybe is rejected there, which is a reason to validate.
Testing a binding
A binding is a contract between the class and the format, and it deserves tests. Keep a sample file for each shape you accept, unmarshal it and assert on every field, including the awkward ones: a book with one author, a book with none, an absent note and an empty note. Add a test that marshals an object and unmarshals the result, and compares. And add a test for the failure path, with a deliberately broken document and the strict event handler, that asserts the exception. When the format changes upstream, the failing test is the first place you will hear about it, and the sample file is the evidence you attach to the conversation with the sender.
What else to know
Namespaces. Namespaced documents need @XmlRootElement(namespace = "...") or a package-level @XmlSchema annotation, and a mismatch results in an unexpected element error or silently empty fields.
Unknown elements. By default JAXB ignores elements and attributes it has no field for. That is forgiving for evolving feeds, and it means a renamed element becomes a null field without an error.
Generated classes. If you have an XSD, xjc generates the classes, which keeps the Java and the schema in step. Hand-written classes are fine for small formats.
Security. The unmarshaller creates its own parser unless you give it a source. To control the parser settings, build an XMLStreamReader with DTDs and external entities disabled and call unmarshal(reader) (disabling DTDs and external entities in Java and Python).
Large files. Unmarshalling a whole file builds the whole object graph. For big files, stream with StAX and unmarshal one record at a time (streaming large XML files in Java without memory trouble).
Related reading
For the choice among parsing APIs, see DOM versus SAX versus StAX parsing in Java, and for the family of languages, parsing XML in Java, C# and other typed languages. The binding annotations are documented in the Jakarta XML Binding specification, and the implementation in the GlassFish JAXB project. Check a path in the XPath tester.
When JAXB is more than the job needs
You need two or three values. A StAX loop or an XPath query is shorter than three classes.
The shape varies. Documents with optional sections, mixed content or open-ended extension points bind badly. Read them with a tree API.
You cannot add the dependency. A library-free project can use the JDK's own parsers. The dependency is small, and the packaging change for Jakarta is real work in older applications.
You need strict data. If bad values must be errors, make the strictness explicit with an event handler and a schema, or read with a parser that fails on bad input.


