JAXB element that is both optional and nillable
Asked Answered
E

3

12

I have re-formatted the question to hopefully make my intentions clearer.

Architecture
I'm writing some web services that I will be publishing myself using JAX-WS. The process we have been using for some time is to first write a schema that only defines the request and response objects. This gets sent to the customer to approve the structure of the xml messages. I don't want to write the whole wsdl myself as it's more complicated than the basic schema.

Next I use the JAXB command xjc to generate classes based on the request and response types in my schema. I then use this classes as parameters and return types on a JAX-WS annotated endpoint class.

This now gives me a web service I can call. It gives me more control over the xml being sent and returned but also automates the repetition required in writing the full wsdl.

Problem
In the schema I have an element like this:

<xs:element name="myElement" type="xs:string" nillable="true" minOccurs="0" /> 

So I want to distinguish between the user setting null or blank. The generated class then has this attribute.

@XmlElementRef(name = "myElement", namespace = "/mynamespace", type = JAXBElement.class)
protected JAXBElement<String> myElement;

The effect of this is that the element becomes neither nillable or optional. The schema that JAX-WS writes as part of the wsdl has set the element to mandatory and not nillable and if I turn off schema validation I still can't pass nil through to my object.

Things tried
If I change it to be required and nillable then I get this generated code.

@XmlElement(required = true, nillable = true)
protected String myElement;

If I change it to optional and not nillable then I get this generated code.

protected String myElement

So you can have either or but not both it seems if you use JAXB. Thoroughly disappointing!

I've also tried manually changing the generated class to look like this.

@XmlElementRef(name = "myElement", namespace = "/mynamespace", type = JAXBElement.class, required=false)
protected JAXBElement<String> myElement;

This now makes the element optional but I still can't set it to nil. Doing so results in a JAXBElement with a value of a blank string. That's only if you turn schema validation off as the resulting JAX-WS wsdl/schema doesn't set the element as nillable, so its not a valid request.

Summary
It's my belief that this is a bug with JAXB. The @XmlElementRef annotation has an attribute to set it as not required but there is no attribute to set the field as nullable.

The @XmlElement annotation has attributes for both required and nullable but these just result in a null object so there would be no way to distinguish between an element not included in the xml or an element that was included but null. This is why you need to use @XmlElementRef along with JAXBElement.

I think the bug includes two issues. First the xjc command should generate the element with required=false. Second there should be an attribute on @XmlElementRef to set whether the element is nullable and this should be set too.

Does anyone know of a fix/workaround? I tried googling but only found people asking the same question without an answer. This usually means it's not possible... TIA.

Additional
I'm using jaxb 2.2.6 and the maven plugin is jaxb2-maven-plugin 1.5.

Evannia answered 29/10, 2013 at 17:48 Comment(0)
W
13

TL;DR

For

@XmlElementRef(name="foo", required=false)
protected JAXBElement<String> foo;

An absent node in the document will correspond to this field being null. An XML element present in the document with xsi:nil="true" will correspond to the value being an instance of JAXBElement with a value of null.

You can also provide an XML schema instead of having JAXB generate one using the location property on the package level @XmlSchema annotation.

@XmlSchema(
    ...
    location="http://www.example.com/schema/root.xsd")
package forum19665550;

import javax.xml.bind.annotation.XmlSchema;

Marshal/Unmarshal

Java Model

Root

This is an object with two fields that can represent optional and nillable data.

import javax.xml.bind.JAXBElement;
import javax.xml.bind.annotation.*;

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

    @XmlElementRef(name="foo", required=false)
    protected JAXBElement<String> foo;

    @XmlElementRef(name="bar", required=false)
    protected JAXBElement<String> bar;

}

ObjectFactory

import javax.xml.bind.JAXBElement;
import javax.xml.bind.annotation.*;
import javax.xml.namespace.QName;

@XmlRegistry
public class ObjectFactory {

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

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

}

Demo Code

Demo

The demo code below will investigate the differences in the values for foo and bar. You can use the JAXBIntrospector class to get the real value for an instance of JAXBElement. There is a bug in EclipseLink JAXB (MOXy) related to unmarshalling an instance of JAXBElement wrapping a null value (see: http://bugs.eclipse.org/420746).

import java.io.File;
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/forum19665550/input.xml");
        Root root = (Root) unmarshaller.unmarshal(xml);

        System.out.println("foo was set:          " + (root.foo != null));
        System.out.println("bar was set:          " + (root.bar != null));
        System.out.println("foo value:            " + root.foo);
        System.out.println("bar value:            " + root.bar);
        System.out.println("foo unwrapped value:  " + JAXBIntrospector.getValue(root.foo));
        System.out.println("bar unwrapped value:  " + JAXBIntrospector.getValue(root.bar));

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

}

input.xml/Output

In the resulting output we see that we can differentiate between an element being absent from the document and an element with `xsi:nil="true" and still have the resulting value be null.

foo was set:          false
bar was set:          true
foo value:            null
bar value:            javax.xml.bind.JAXBElement@4af42ea0
foo unwrapped value:  null
bar unwrapped value:  null
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<root>
    <bar xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="true"/>

Generating an XML Schema

Demo Code

GenerateSchema

Below is some JAXB code that will generate an XML Schema from the annotated model.

import java.io.IOException;
import javax.xml.bind.*;
import javax.xml.transform.Result;
import javax.xml.transform.stream.StreamResult;

public class GenerateSchema {

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

        jc.generateSchema(new SchemaOutputResolver() {

            @Override
            public Result createOutput(String namespaceUri,
                    String suggestedFileName) throws IOException {
                StreamResult result = new StreamResult(System.out);
                result.setSystemId(suggestedFileName);
                return result;
            }

        });
    }

}

Output

Here is the resulting XML Schema. You are correct that it doesn't indicate that the foo and bar elements are nillable.

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<xs:schema version="1.0" xmlns:xs="http://www.w3.org/2001/XMLSchema">

  <xs:element name="bar" type="xs:string"/>

  <xs:element name="foo" type="xs:string"/>

  <xs:element name="root" type="root"/>

  <xs:complexType name="root">
    <xs:sequence>
      <xs:element ref="foo" minOccurs="0"/>
      <xs:element ref="bar" minOccurs="0"/>
    </xs:sequence>
  </xs:complexType>
</xs:schema>

Providing an XML Schema

Instead of having JAXB derive an XML Schema from your model, you can point to your existing one that will contain more information.

package-info

This is done by specifying the location property on the package level @XmlSchema annotation.

@XmlSchema(
    ...
    location="http://www.example.com/schema/root.xsd")
package forum19665550;

import javax.xml.bind.annotation.XmlSchema;
Widener answered 29/10, 2013 at 18:23 Comment(4)
I think you've misunderstood. First this is a request so it's marshaling xml into an object, not the other way round. Second when I use this generated class as part of a web service the wsdl shows this element as both required and not nillable. I don't see why this would be any different on an output. It might be possible to set the JAXBElement to null but the response wouldn't validate against the wsdl/schema.Evannia
Thanks anyway, I've reworded slightly to hopefully make it clearer.Evannia
@BenThurley - I have updated my answer to hopefully better match your question.Widener
I can't easily point it at a schema. I'm using JAX-WS to deploy this as a web service so JAX-WS is using the JAXB annotations to create a wsdl. I could write the wsdl by hand and point it at that but I don't want to do that.Evannia
M
5

You can customise the binding by

<jaxb:globalBindings generateElementProperty="false" />

As documented in Customized Binding , for the same exact case you are asking about.

I'm using custom binding with maven plugin org.jvnet.jaxb2.maven2:maven-jaxb2-plugin

Muddy answered 26/8, 2015 at 10:4 Comment(0)
K
3

As I understand you Ben the following XSD:

<xs:element name="myElement" type="xs:string" nillable="true" minOccurs="0" /> 

Should result in:

@XmlElementRef(name = "myElement", namespace = "/mynamespace", type = JAXBElement.class, required = false)
protected JAXBElement<String> myElement;

Right?

But for default JAXB implementation it is not the case. Looks like a bug in JAXB. I didn't find it in JAXB issue tracker. required attribute was introduced to @XmlElementRef in JAXB 2.2 in around 2009 but apparently no one created issue for this problem.

required attribute cannot be changed using Binding Customizations.

In this situation you can:

  • write your own plugin for XJC to add missing attribute to @XmlElementRef annotation. It is not that difficult. More information here.
  • use alternative JAXB implementation (MOXy works fine - required = false is generated using MOXy JAXB compiler)
  • wait for Oracle implementation of JAXB to be fixed.

No matter which option you choose please raise issue in JAXB issue tracker so that the problem will be fixed.

EDIT:

To show that creating plugin is easy I created one. You can find it in my github repository. Feel free to use/copy/modify at will. I don't give guarantee that it works in 100% but for simple cases works like a charm.

EDIT2:

If schema generated based on java objects and JAXB annotations does not match your interface then you can use @WebService.wsdlLocation to point to your original, correct WSDL and XSD files.

EDIT3:

It's weird that nil is ignored by JAXB in your case. I ran a test using JAXB 2.2.6 and 2.2.7 and nil is correctly recognized:

JAXBContext context = JAXBContext.newInstance(SomeElement.class);
Unmarshaller unmarshaller = context.createUnmarshaller();
String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><ns2:someElement xmlns:ns2=\"http://www.example.org/example/\"><myElement xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:nil=\"true\"/></ns2:someElement>";
SomeElement someElement = (SomeElement) unmarshaller
        .unmarshal(new StringReader(xml));
assertThat(someElement.getMyElement().isNil(), is(true));

Could you check whether you correctly set nil attribute, e.g.:

<myElement xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="true"/>

If it is correct please try to run the test with your class.

Kearse answered 30/10, 2013 at 20:26 Comment(5)
I already tried manually adding the required=false attribute. This makes it optional but still not nillable. Even if I turn off schema validation it still isn't setting the JAXBElement as nill. i.e. getNil returns false. I may have to raise the bug as you suggest.Evannia
Sorry, but I don't get what is setting the JAXBElement as nill. Do you mean that input messages are not unmarshalled correctly? Btw. I updated my answer so that you can work around problem of incorrect XML schema being generated.Kearse
Yes, if you call the web service passing in the element as nil then it is not reflected in the resulting JAXBElement. If you call getValue on the JAXBElement you get a blank string. If you call isNil you get false. It also fails validation against the schema so I had to turn it off to get that far. Setting the wsdl location isn't really an option because I only have a schema to define the request and response types. I want JAX-WS to generate the rest of the wsdl for me. From what I can tell @XmlElementRef doesn't support both optional and nillable. It only has an attribute to set as required.Evannia
I cannot reproduce the problem with JAXB unmarshalling nil incorrectly. Please take a look at my latest edit.Kearse
I used your example to generate some xml and then convert back into an object. I had to fiddle with it a bit but eventually I was able to get it marshalling and unmarshalling. However, I still come unstuck when it comes to schema validation because the element is not nillable in the schema. I don't know if this is an issue with JAX-WS or JAXB.Evannia

© 2022 - 2024 — McMap. All rights reserved.