Sergio
Sergio

Reputation: 8672

XmlAdapter not working as expected in JAXB RI

I am trying to implement a XmlAdapter for modifying the marshalling/unmarshalling of certain object properties. Particularly, I tried with the NullStringAdapter described here:

Jaxb: xs:attribute null values

The objective of the NullStringAdapter is marshalling null values as empty strings, and viceversa.

The only difference with the example described above and my code, is that I want to apply the adapter to an element, not to an attribute, so what I have is:

@XmlElement
@XmlJavaTypeAdapter(NullStringAdapter.class)
public String getSomeValue() {
    return someValue;  //someValue could be null, in that case the adapter should marshall it as an empty string
}

However, after some debugging, I realized that the Adapter methods are never called during the marshalling from Java to XML!. This occurs when the XmlElement value is null. When this value is different than null, the adapter methods are called as expected.

Thanks for any help!.

Upvotes: 3

Views: 7859

Answers (2)

bdoughan
bdoughan

Reputation: 148977

Note: I'm the EclipseLink JAXB (MOXy) lead, and a member of the JAXB 2 (JSR-222) expert group.

However, after some debugging, I realized that the Adapter methods are never called during the marshalling from Java to XML!. This occurs when the XmlElement value is null. When this value is different than null, the adapter methods are called as expected.

This behaviour varies between implementations of JAXB. The JAXB reference implementation will not call the marshal method on the XmlAdapter when the field/property is null, but MOXy will.

What the JAXB spec says (section 5.5.1 Simple Property)

The get or is method returns the property’s value as specified in the previous subsection. If null is returned, the property is considered to be absent from the XML content that it represents.

The MOXy interpretation of this statement is that the value of the field/property is really the value once it has gone through the XmlAdapter. This is necessary to support the behaviour that Sergio is looking for.

Upvotes: 7

G_H
G_H

Reputation: 11999

Of course the adapter will never be called if there isn't any element in the input to trigger that action. What happens in that example you're linked is that an attribute with an empty value is presented:

<element att="" />

The key here is that there is an att attribute, but it has an empty String. So a JAXB unmarshaller is gonna present that to the setter. But, since there's an adapter declared on it, it will pass through there and get turned into a null value.

But if you had this

<element />

it's another story. There's no att attribute, so the setter would never need to be called.

There's a difference between an element that occurs but has no content and a complete absence of an element. The former can basically be considered to contain an empty String, but the latter is just "not there".

EDIT: tested with these classes...

Bean.java

package jaxbadapter;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name="Test")
public class Bean {

    @XmlElement
    @XmlJavaTypeAdapter(NullStringAdapter.class)
    private String someValue;

    public Bean() {
    }

    public String getSomeValue() {
        return someValue;
    }

    public void setSomeValue(final String someValue) {
        this.someValue = someValue;
    }

}

NullStringAdapter.java

package jaxbadapter;

import javax.xml.bind.annotation.adapters.XmlAdapter;

public class NullStringAdapter extends XmlAdapter<String, String> {

    @Override
    public String unmarshal(final String v) throws Exception {
        if("".equals(v)) {
            return null;
        }
        return v;
    }

    @Override
    public String marshal(final String v) throws Exception {
        if(null == v) {
            return "";
        }
        return v;
    }

}

ObjectFactory.java

package jaxbadapter;

import javax.xml.bind.JAXBElement;
import javax.xml.bind.annotation.XmlElementDecl;
import javax.xml.bind.annotation.XmlRegistry;
import javax.xml.namespace.QName;


@XmlRegistry
public class ObjectFactory {

    public ObjectFactory() {
    }

    public Bean createBean() {
        return new Bean();
    }

    @XmlElementDecl(name = "Test")
    public JAXBElement<Bean> createTest(Bean value) {
        return new JAXBElement<>(new QName("Test"), Bean.class, null, value);
    }

}

Main.java

package jaxbadapter;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.Marshaller;
import javax.xml.transform.stream.StreamResult;

public class Main {

    public static void main(String[] args) throws Exception {

        final JAXBContext context = JAXBContext.newInstance("jaxbadapter");

        final Marshaller m = context.createMarshaller();
        m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);

        final ObjectFactory of = new ObjectFactory();

        final Bean b1 = new Bean();

        final Bean b2 = new Bean();
        b2.setSomeValue(null);

        final Bean b3 = new Bean();
        b3.setSomeValue("");

        m.marshal(of.createTest(b1), System.out);
        System.out.println("");

        m.marshal(of.createTest(b2), System.out);
        System.out.println("");

        m.marshal(of.createTest(b3), System.out);
        System.out.println("");


    }

}

This is the output:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Test/>

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Test/>

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Test>
    <someValue></someValue>
</Test>

Actually surprised me quite a bit. I've then tried changing the getter to return someValue == null ? "" : someValue; to no avail. Then set a breakpoint on the getter and found out it never gets called.

Apparently JAXB uses reflection to try and retrieve the value rather than going through the setter when using XmlAccessType.FIELD. Hardcore. Now, you can bypass this by using XmlAccessType.PROPERTY instead and annotating either the getter or setter...

package jaxbadapter;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;

@XmlAccessorType(XmlAccessType.PROPERTY)
@XmlType(name="Test")
public class Bean {

    private String someValue;

    public Bean() {
    }

    @XmlElement
    @XmlJavaTypeAdapter(NullStringAdapter.class)
    public String getSomeValue() {
        return someValue;
    }

    public void setSomeValue(final String someValue) {
        this.someValue = someValue;
    }

}

... but that still didn't help. The adapter's marshal method was only called once, on the last test case where an empty String had been set. Apparently it first calls the getter and when that returns null, it simply skips the adapter stuff altogether.

The only solution I can come up with is just foregoing the use of an adapter altogether here and put the substitution in the getter, making sure to use XmlAccessType.PROPERTY:

package jaxbadapter;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;

@XmlAccessorType(XmlAccessType.PROPERTY)
@XmlType(name="Test")
public class Bean {

    private String someValue;

    public Bean() {
    }

    @XmlElement
//  @XmlJavaTypeAdapter(NullStringAdapter.class)
    public String getSomeValue() {
        return someValue == null ? "" : someValue;
    }

    public void setSomeValue(final String someValue) {
        this.someValue = someValue;
    }

}

That worked for me. It's only really an option if you're creating the JAXB-annotated classes yourself and not generating them via XJC from a schema, though.

Maybe someone can clarify a bit why adapters are skipped for nulls and if there's a way to change that behaviour.

Upvotes: 3

Related Questions