Konstantin Lastochkin
Konstantin Lastochkin

Reputation: 61

XmlID/XmlIDREF and inheritance in latest JAXB version (Java 7)

I am using @XmlID and @XmlIDREF tags to reference one object from another. It was working fine in Java 6 even with inherited classes. The example code I've created looks like this. Base class used tags:

@XmlRootElement
@XmlAccessorType(FIELD)
public class Module {
    Module() {}

    @XmlIDREF
    private Module other;

    @XmlID
    private String id;

    public Module(String id, Module other) {
        this.id = id;
        this.other = other;
    }
}

Inherited class:

@XmlRootElement
public class TheModule extends Module {
    TheModule() {}

    private String feature;

    public TheModule(String id, Module other, String feature) {
        super(id, other);
        this.feature = feature;
    }
}

Container for these classes:

@XmlRootElement
public class Script {
    Script() {}
    public Script(Collection<Module> modules) {
        this.modules = modules;
    }

    @XmlElementWrapper
    @XmlElementRef
    Collection<Module> modules = new ArrayList<Module>();
}

When running this example code:

public class JaxbTest {

    private Script createScript() {
        Module m1 = new Module("Module1", null);
        Module m2 = new TheModule("Module2", m1, "featured  module");
        Module m3 = new Module("Module3", m2);
        return new Script(Arrays.asList(m1, m2, m3));
    }

    private String marshal(Script script) throws Exception {
        JAXBContext context = JAXBContext.newInstance(Module.class, Script.class, TheModule.class);
        Writer writer = new StringWriter();
        context.createMarshaller().marshal(script, writer);
        return writer.toString();
    }

    private void runTest() throws Exception {
        Script script = createScript();

        System.out.println(marshal(script));
    }

    public static void main(String[] args) throws Exception  {
        new JaxbTest().runTest();
    }
}

I receive XML, in Java 6:

<script>
  <modules>
    <module>
      <id>Module1</id>
    </module>
    <theModule>
      <other>Module1</other>
      <id>Module2</id>
      <feature>featured module</feature>
    </theModule>
    <module>
      <other>Module2</other>
      <id>Module3</id>
    </module>
  </modules>
</script>

Note that reference to the m2 (TheModule instance) is serialized as expected. But when the same code is running under Java 7 (Jaxb 2.2.4-1) I receive:

<script>
  <modules>
    <module>
      <id>Module1</id>
    </module>
    <theModule>
      <other>Module1</other>
      <id>Module2</id>
      <feature>featured module</feature>
    </theModule>
    <module>
      <other xsi:type="theModule" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
        <other>Module1</other>
        <id>Module2</id>
        <feature>featured module</feature>
      </other>
      <id>Module3</id>
    </module>
  </modules>
</script>

So you can see that on latest JAXB @XmlIDREF for inherited module is not working!

Upvotes: 5

Views: 1834

Answers (3)

Thomas Schurins
Thomas Schurins

Reputation: 21

If you are still stuck with Java7 in 2017, then there is an easy way to circumvent this. The problem is that when serialiazing with a XmlREFId, Jaxb can't cope with sub-classes. A solution is to use an adapter and return an new instance of the base class with the same id as the one being serialized (only the id will be used, so we don't need to bother with the other fields):

public class ModuleXmlAdapter extends XmlAdapter<Module, Module> {
    @Override
    public Module unmarshal(Module v) {
        // there is no problem with deserializing - return as is
        return v;
    }

    @Override
    public Module marshal(Module v) {
        if (v == null) {
            return null;
        }
        // here is the trick:
        return new Module(v.getId(), null);
    }
}

Then, just use that adapter wherever needed:

public class Module {
    @XmlIDREF
    @XmlJavaTypeAdapter(ModuleXmlAdapter.class)
    private Module other;

    //...
}

Upvotes: 0

Koh&#225;nyi R&#243;bert
Koh&#225;nyi R&#243;bert

Reputation: 10151

This answer is wrong. Instead of relying on it read the documentation for JAXBContext.newInstance(...), see this answer and the comments below.


I think you confuse JAXB with the following line.

JAXBContext.newInstance(Module.class, Script.class, TheModule.class);

You tell it that you want to serialize the XML types Script, Module and TheModule. JAXB will treat objects of this latter type in a special way, because you've already supplied its base class: it adds a discriminating attribute to it. This way these two types can be differentiated in the serialized XML.

Try supplying only Script and Module, the base class for all modules.

JAXBContext.newInstance(Module.class, Script.class);

In fact, you can leave out Module entirely. JAXB will infer types in the Script object you try to serialize from the context.

This behaviour by the way isn't exactly related to Java 6. It's related to the JAXB implementation in use (okay-okay, almost the same thing, I know). In my projects I use JAXB 2.2.4-1, which reproduces the issue at hand in Java 6 and 7 too.

Oh, and one more thing: instead of creating a StringWriter and marshalling objects into that, then sending its contents to System.out you can use the following to send formatted XML to stdout.

Marshaller marshaller = context.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
marshaller.marshal(script, System.out);

Maybe this can ease (a tiny little bit) further testing.

Upvotes: 2

alexsb
alexsb

Reputation: 548

This seems to be a known bug in JAXB. I had the same problem and resolved it by not using XmlIDRef, but rather using postDeserialize with a custom ID.

Here is a bug report:

http://java.net/jira/browse/JAXB-870

I couldn't use Kohányi Róbert's suggestion. Did that work for you?

Upvotes: 0

Related Questions