java unmarshalling - NULL value or missing tag?

24.4k views Asked by At

I have a nillable field in a class that is being set by the unmarshaller:

@XmlElement(name = "value", nillable = true)
private BigDecimal valueVariable;

My problem is that I can't tell if the xml element has been omitted or set to nil:

A. element <value/> is missing from the XML file, it is not required.
=> (valueVariable == null) is true

B. XML file contains <value xsi:nil="true"/>
=> (valueVariable == null) is true

How can I tell for a non-String variable if the value is xsi:nil or the tag is missing?

UPDATE You can see 2 good solutions, I preferred one of them, but the other would also be fine!

3

There are 3 answers

4
bdoughan On BEST ANSWER

JAXB (JSR-222) implementations can represent null as either an absent node or a nillable element based on the nillable setting on @XmlElement. When you need to support both, or differentiate between the two then you can leverage JAXBElement.

Java Model

Root

Fields/properties of type JAXBElement are mapped with the @XmlElementRef annotation. This corresponds to @XmlElementDecl annotations on a class annotated with @XmlRegistry.

import java.math.BigDecimal;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.annotation.*;

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Root {

    @XmlElementRef(name="foo")
    JAXBElement<BigDecimal> foo;

    @XmlElementRef(name="bar")
    JAXBElement<BigDecimal> bar;

    @XmlElementRef(name="baz")
    JAXBElement<BigDecimal> baz;

}

ObjectFactory

import java.math.BigDecimal;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.annotation.*;
import javax.xml.namespace.QName;

@XmlRegistry
public class ObjectFactory {

    @XmlElementDecl(name = "foo")
    public JAXBElement<BigDecimal> createFoo(BigDecimal value) {
        return new JAXBElement<BigDecimal>(new QName("foo"), BigDecimal.class, value);
    }

    @XmlElementDecl(name = "bar")
    public JAXBElement<BigDecimal> createBar(BigDecimal value) {
        return new JAXBElement<BigDecimal>(new QName("bar"), BigDecimal.class, value);
    }

    @XmlElementDecl(name = "baz")
    public JAXBElement<BigDecimal> createBaz(BigDecimal value) {
        return new JAXBElement<BigDecimal>(new QName("baz"), BigDecimal.class, value);
    }

}

Demo Code

input.xml

<?xml version="1.0" encoding="UTF-8"?>
<root>
    <bar xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="true"/>
    <baz>123.456</baz>
</root>

Demo

Below is some demo code you can run to show that everything works. Note how the JAXBIntrospector can be used to get the real value unwrapping the JAXBElement if necessary.

import java.io.File;
import java.math.BigDecimal;
import javax.xml.bind.*;

public class Demo {

    public static void main(String[] args) throws Exception {
        JAXBContext jc = JAXBContext.newInstance(Root.class, ObjectFactory.class);

        Unmarshaller unmarshaller = jc.createUnmarshaller();
        File xml = new File("src/forum18440987/input.xml");
        Root root = (Root) unmarshaller.unmarshal(xml);

        nullOrAbsent("foo", root.foo);
        nullOrAbsent("bar", root.bar);
        nullOrAbsent("baz", root.baz);

        Marshaller marshaller = jc.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.marshal(root, System.out);
    }

    private static void nullOrAbsent(String property, JAXBElement<BigDecimal> value) {
        System.out.print(property);
        if(null == value) {
            System.out.print(":  ABSENT - ");
        } else if(value.isNil()) {
            System.out.print(":  NIL - ");
        } else {
            System.out.print(":  VALUE - ");
        }
        System.out.println(JAXBIntrospector.getValue(value));
    }

}

Output

foo:  ABSENT - null
bar:  NIL - null
baz:  VALUE - 123.456
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<root>
    <bar xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="true"/>
    <baz>123.456</baz>
</root>

UPDATE

If you wanted to maintain your existing get/set methods, then you could keep field access as I have in this answer and change your accessor methods to look like the following:

public BigDecimal getBar() {
    if(null == bar) {
        return null;
    }
    return bar.getValue();
}

public void setBar(BigDecimal bar) {
    if(null == this.bar) {
        this.bar = new JAXBElement<BigDecimal>(new QName("bar"), BigDecimal.class, bar);
    } else {
        this.bar.setValue(bar);
    }
}

Additionally you could add an isSet method to see if the value had been set.

public boolean isSetBar() {
    return null != bar;
}

This approach does not require that you have access to the Unmarshaller. To make sure that the ObjectFactory is picked up you can use the @XmlSeeAlso annotation to reference it from one of your domain classes.

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
@XmlSeeAlso(ObjectFactory.class)
public class Root {
0
Xavi López On

There's a similar question here: Handling missing nodes with JAXB. It seems that if the element is missing, the setter method won't be called. So you could put some logic in the method to determine if the node is missing or has an empty value.

@XmlElement(name = "value", nillable = true)
private BigDecimal valueVariable;

private boolean valueVariableMissing = true;

public void setValueVariable(BigDecimal valueVariable){
    this.valueVariable = valueVariable;
    this.valueVariableMissing = false;
}
0
Dimitris On

I was dealing with the same issue, In order to fix it i created a new file in the package were I was marshaling the object:
"package-info.java" There I added:

@javax.xml.bind.annotation.XmlSchema(namespace = "http://the.correct.namespace.com/gateway/", elementFormDefault = javax.xml.bind.annotation.XmlNsForm.QUALIFIED)
package com.the.package.name;

Now everything is ok