ysfaran
ysfaran

Reputation: 6962

JAXB marshalling: treat empty object like it's null

I want to explain my issue with a simple example:

Foo:

@SomeXMLAnnotations
public class Foo {
    // Bar is just a random class with its own XML annotations
    @XmlElement(required = true)
    Bar someBarObj;

    boolean chosen = true;
    boolean required = true;

    public Foo(){ 
        chosen = false;
    }

    public Foo(Bar someBarObj){
        this.someBarObj = someBarObj;
    }
}

MyClass:

@SomeXMLAnnotations
public class MyClass {

    @XmlElement(required = false)
    Foo anyFooObj;

    @XmlElement(required = true)
    Foo anyFooObjRequired;

    public MyClass (){ }

    public MyClass (Foo anyFooObj, Foo anyFooObjRequired){
        this.anyFooObj = anyFooObj;
        if(anyFooObj == null)
            this.anyFooObj = new Foo();
        /*
         * This is the reason why i can't let 'anyFooObj' be 'null'.
         * So 'anyFooObj' MUST be initialized somehow.
         * It's needed for some internal logic, not JAXB.
         */
        anyFooObj.required = false;

        this.anyFooObjRequired = anyFooObjRequired;
    }
}

Example Objects:

Foo fooRequired = new Foo(new Bar());
MyClass myObj = new MyClass(null, fooRequired); 

When i try to marshal myObj now, it throws an exception like this:

org.eclipse.persistence.oxm.record.ValidatingMarshalRecord$MarshalSAXParseException; 
cvc-complex-type.2.4.b: The content of element 'n0:anyFooObj ' is not complete.
One of '{"AnyNamespace":someBarObj}' is expected.

This happens because anyFooObj is initialized but it's required, member someBarObj isn't.

Possible Solution:

I know i could add this method to MyClass:

void beforeMarshal(Marshaller m){
    if(! anyFooObj.chosen)
        anyFooObj= null;
    }
}

But I have a lot of classes and those classes have a lot of not required fields. So this solution would take ages and doesn't look like a proper solution as well.

My Question:

Is there a way to tell JAXB that it should treat empty objects like they were null? Or that it should ignore an element when it's not properly set. Something like this for example:

@XmlElement(required = false, ingnoreWhenNotMarshallable = true)
Foo anyFooObj;

NOTE:

I'm NOT the developer of the code. I just have to add JAXB to the project and make everything compatible with a given XSD file. I'm NOT allowed to change the relation between classes.

Upvotes: 0

Views: 1851

Answers (1)

Peter
Peter

Reputation: 965

I think you're trying to make the JAXB marshaller do something it's really not designed to do, so I'd say you're into hack territory here. I'd recommend pushing back on the requirements to try and avoid having this problem in the first place.

That said, if you have to do it then given your requirement to avoid writing code for each class/field, I think you'll want to use reflection for this - I've included an example below that reflectively inspects the values of all fields.

Useful extensions would be:

  • Have it consider getter methods too
  • Make the null-setting behaviour opt-in by requiring the field has an additional annotation - you could name it @JAXBNullIfEmpty

Example.java:

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import java.io.StringWriter;
import java.lang.reflect.Field;

public class Example
{
    public abstract static class JAXBAutoNullifierForEmptyOptionalFields
    {
        void beforeMarshal(Marshaller x)
        {
            try
            {
                for (Field field : this.getClass().getFields())
                {
                    final XmlElement el = field.getAnnotation(XmlElement.class);

                    // If this is an optional field, it has a value & it has no fields populated then we should replace it with null
                    if (!el.required())
                    {
                        if (JAXBAutoNullifierForEmptyOptionalFields.class.isAssignableFrom(field.getType()))
                        {
                            final JAXBAutoNullifierForEmptyOptionalFields val = (JAXBAutoNullifierForEmptyOptionalFields) field.get(
                                    this);

                            if (val != null && !val.hasAnyElementFieldsPopulated())
                                field.set(this, null); // No fields populated, replace with null
                        }
                    }
                }
            }
            catch (IllegalAccessException e)
            {
                throw new RuntimeException("Error determining if class has all required fields: " + this, e);
            }
        }


        boolean hasAnyElementFieldsPopulated()
        {
            for (Field field : this.getClass().getFields())
            {
                try
                {
                    if (field.isAnnotationPresent(XmlElement.class))
                    {
                        // Retrieve value
                        final Object val = field.get(this);

                        // If the value is non-null then at least one field has been populated
                        if (val != null)
                        {
                            return true;
                        }
                    }
                }
                catch (IllegalAccessException e)
                {
                    throw new RuntimeException("Error determining if class has any populated JAXB fields: " + this, e);
                }
            }

            // There were no fields with a non-null value
            return false;
        }
    }

    @XmlRootElement
    public static class MyJAXBType extends JAXBAutoNullifierForEmptyOptionalFields
    {
        @XmlElement
        public String someField;

        @XmlElement
        public MyJAXBType someOtherField;


        public MyJAXBType()
        {
        }


        public MyJAXBType(final String someField, MyJAXBType someOtherField)
        {
            this.someField = someField;
            this.someOtherField = someOtherField;
        }
    }


    public static void main(String[] args) throws Exception
    {
        final Marshaller marshaller = JAXBContext.newInstance(MyJAXBType.class).createMarshaller();

        MyJAXBType innerValue = new MyJAXBType(); // Unpopulated inner value
        MyJAXBType value = new MyJAXBType("some text value", innerValue);

        final StringWriter sw = new StringWriter();

        marshaller.marshal(value, sw); // Omits "someOtherField"

        System.out.println(sw.toString());
    }
}

Upvotes: 2

Related Questions