November 5, 2010

JAXB and Inheritance - MOXy Extension @XmlDescriminatorNode/@XmlDescrimintatorValue

In previous blog posts I described how to leverage the xsi:type attribute and substitution groups to represent inheritance.  In this post I'll demonstrate how an upcoming EclipseLink 2.2 JAXB (MOXy) extension can be used to represent inheritance.  It leverages an attribute of your own choosing to represent the subtype.  If you are using a version of EclipseLink prior to 2.2 you can use the technique described here.

Java Model 

The model will contain an abstract super class for all types of contact information. We will use MOXy's @XmlDescriminatorNode annotation to specify the XML attribute we wish to use to indicate the appropriate subtype.

package blog.inheritance;

import org.eclipse.persistence.oxm.annotations.XmlDiscriminatorNode;

@XmlDiscriminatorNode("@classifier")
public abstract class ContactInfo {

}

Address and PhoneNumber will be the concrete implementations of ContactInfo. By default the type name will be used with the discriminator node, we can override this by using @XmlDescriminatorValue.

package blog.inheritance;

import org.eclipse.persistence.oxm.annotations.XmlDiscriminatorValue;

@XmlDiscriminatorValue("address-classifier")
public class Address extends ContactInfo {

    private String street;

    public String getStreet() {
        return street;
    }

    public void setStreet(String street) {
        this.street = street;
    }

}

package blog.inheritance;

import org.eclipse.persistence.oxm.annotations.XmlDiscriminatorValue;

@XmlDiscriminatorValue("phone-number-classifier")
public class PhoneNumber extends ContactInfo {

}

The Customer object can have different types of contact info set on it, so the property will refer to the super class.

package blog.inheritance;

import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class Customer {

    private ContactInfo contactInfo;

    public ContactInfo getContactInfo() {
        return contactInfo;
    }

    public void setContactInfo(ContactInfo contactInfo) {
        this.contactInfo = contactInfo;
    }

}

Java Model - jaxb.properties

In order to specify that we are using the MOXy JAXB implementation we need to put a file called jaxb.properties in with our Address class with the following entry: 

javax.xml.bind.context.factory=org.eclipse.persistence.jaxb.JAXBContextFactory

Demo Code

We will use the following demo code to demonstrate the use of the descriminator node to represent inheritance.

package blog.inheritance;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;

public class Demo {

    public static void main(String[] args) throws Exception {
        Customer customer = new Customer();
        Address address = new Address();
        address.setStreet("1 A Street");
        customer.setContactInfo(address);

        JAXBContext jc = JAXBContext.newInstance(Customer.class, Address.class, PhoneNumber.class);

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

}

 XML

The following is the resulting XML document.  Note that the Address object is marshalled to the contactInfo element with the classifier attribute containing the discriminator node value "address-classifier".


<customer>
   <contactInfo classifier="address-classifier">
      <street>1 A Street</street>
   </contactInfo>
</customer>

How does this All Work?

This is very similar to using the xsi:type attribute to represent the sub type.  However in this case we are not leveraging an XML schema concept.  This is not necessarily a bad thing, as there are legitimate use cases where this type of behaviour is required (check out this Stack Overflow post for an example).  It is also useful when you need to interact with XML binding tools with proprietary mechanisms for handling inheritance.

42 comments:

  1. Can PhoneNumber/Address be also @XmlRootElement?

    So will this example work:
    Address address = new Address();
    address.setStreet("1 A Street");

    JAXBContext jc = JAXBContext.newInstance(Address.class, PhoneNumber.class);

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

    ?

    ReplyDelete
  2. Andrew,

    Yes, this concept can also be used when the classes in the inheritance hierarchy (Address & PhoneNumber in this example) are the root objects.

    -Blaise

    ReplyDelete
  3. Which jar files I need to have in classpath to use MOXy, if I use latest Oracle JDK 1.6u22?

    Is eclipselink.jar enough?

    Classes from eclipselink\jlib\moxy (as I can see) already exist in jdk, but your are recent.
    Do I need them or old-bundled-in-JDK are enough?

    ReplyDelete
  4. Andrew,

    When using JDK 1.6 only eclipselink.jar is required. The supporting jars are provided for those using JDK 1.5 (which does not include items like the JAXB public API).

    -Blaise

    ReplyDelete
  5. But classes in your supporting jars are newer than bundled, may be they are more stable, faster, etc? ;-)
    Or one can use only eclipselink.jar without any doubts?

    ReplyDelete
  6. Andrew,

    With JDK 1.6 I would still use the versions of the classes it contains. You can override the libraries in the JDK using the endorsed mechanism, but it is not necessary in order to use MOXy

    -Blaise

    ReplyDelete
  7. Thank You Blaise!

    Your blog and your support are outstanding!

    ReplyDelete
  8. Excellent blog,
    I'm trying to use XmlID and XmlIDRef with Inheritence. Eg my class with id attribute annotated with XmlID inherits from an abstract base class. However I keep getting ArrayIndexOutOfBoundsException -1. The same error happens with the xsi:type and xmldescriminator methods. Any ideas?

    ReplyDelete
  9. Hi Denis,

    Can you provide more details on your use case through the "Contact Me" page?:

    -Blaise

    ReplyDelete
  10. Is it possible to "discriminate" based on the value of a child element rather than on an attribute?

    ReplyDelete
  11. Hi Denis,

    Thank you for sending the details of your use case. I haven't been able to reproduce the issue, but have emailed you what I have tried so far.

    -Blaise

    ReplyDelete
  12. Hi Kevin,

    Currently the descriminator node must be an XML attribute. I have opened an enhancement request to expand this feature to cover child elements:

    - Bug 341149 - Enhancement: Expand @XmlDescriminatorNode to support child elements

    -Blaise

    ReplyDelete
  13. What I do not understand is the following:
    Coding a line like this:

    JAXBContext jc = JAXBContext.newInstance(Customer.class, Address.class, PhoneNumber.class);

    fully breaks the OO design. I do not want to hardcode all existing subclasses of ContactInfo.class, I would expect that JAXB finds the correct subclass by itself. Is this not possible?

    Cheers
    Balz

    ReplyDelete
  14. Hi Balz,

    Using Java reflection you can not ask a class for all of its subclasses. Some people leverage the @XmlSeeAlso annotation on the parent class, and specify the subclasses there to avoid including them in the creation of the JAXBContext.

    -Blaise

    ReplyDelete
  15. Hello,

    I have a problem when I write the model to output.
    I have downladed the EclipseLink Target Component and I started to copy your example. But when the JAXB write to the output, he does not write descriminator attribute but xmlns:xsi=... instead.

    What is wrong ?

    Thank You for your help

    Laurent

    ReplyDelete
  16. Hello Bibi,

    Have you included the jaxb.properties file? The jaxb.properties file is what specifies that EclipseLink JAXB (MOXy) should be used as the JAXB provider:

    - Specifying EclipseLink MOXy as Your JAXB Provider

    -Blaise

    ReplyDelete
  17. Hi Bibi/Laurent,

    Have you correctly specified EclipseLink JAXB (MOXy) as the JAXB provider? This is done via a jaxb.properties file:

    - Specifying EclipseLink MOXy as Your JAXB Provider

    -Blaise

    ReplyDelete
  18. Hello Blaise,

    When I try to add the "org.eclipse.persistence.moxy" plugin, the application can not start. Any idea ?

    Thank You

    Best regards

    bibi

    ReplyDelete
  19. Hi Bibi,

    What exception are you getting when you try to start your application? You will need to ensure that you have all the supporting bundles:
    - Required bundles for MOXy

    If you are using MOXy in a GlassFish environment the following may help:
    - Creating a RESTful Web Service - Part 3/5

    -Blaise

    ReplyDelete
  20. Hi Blaise,
    I think I have the same or similar question than Denis O'Sullivan. Would you please post what you found out so far?

    Thanks
    Torsten

    ReplyDelete
  21. I switched to MOXy to leverage the XmlCDATA annotation, but now I see that inheritance works differently from the stock marshaller.

    In my current implementation I have @XmlTransient, as well as several @XmlElement fields, on the super class. This way I can use fields from the super class in the propOrder of @XmlType in the children classes.

    How do I use fields from the super class in MOXy, and how do I order them in the children classes' @XmlType(propOrder)?

    ReplyDelete
  22. Hi Torsten,

    Denis was able to get around the issue he was seeing by marking the base class @XmlTransient. Could you let me know if this would work for your use case?

    -Blaise

    ReplyDelete
  23. Hi Henrik,

    You should get the same behaviour in MOXy. Which version are you using?

    For more information:
    - Ignoring Inheritance with @XmlTransient

    -Blaise

    ReplyDelete
  24. Hi Blaise,

    Thanks for your fine article.

    My problem for the week is:

    How to serialize a java base class and set the Xml-element name to a value that I'll know only at runtime?

    Any chance to do that?

    kind regards Franz-Josef

    ReplyDelete
  25. Hi Franz-Josef,

    I apologize for the delay in responding, it has been one of those weeks. I'm not sure that I fully understand the question, but you should be able to supply the XML element name via an instance of JAXBElement (you would need to make your field/property of this type).

    Do you have a more concrete example you could share?

    -Blaise

    ReplyDelete
  26. hi, im using jaxb with json and i have useed the @XmlDescriminatorNode/@XmlDescrimintatorValue as mentioned in this post, but its not working, it always take only the super class.

    here is the sample json -
    { "foo" : { "properties": [{"type":"t1", "value":"this is string"},{"type":"t2", "value":"1234"}]}}

    - here properties is an array and i have class A and its subclasses X & Y are for t1 & t2 respectively.

    i have used @XmlDescriminatorNode(value="type") on class A and @XmlDescriminatorValue(value="t1") on X and @XmlDescriminatorValue(value="t2") on Y respectively.

    my jaxb.properties is getting read properly too.

    im not able to crack this problem!! any advice on how to solve this? any work around?

    thanks for the article.

    ReplyDelete
  27. also, if i make my super class as an abstract class im getting the error "Missing class indicator field from database row [UnmarshalRecord()]." - no idea!

    ReplyDelete
  28. Hi Jasphior,

    When you build your JAXBContext is it aware of both the X and Y classes? This can be done by passing these classes in when the JAXBContext is created or by using the @XmlSeeAlso annotation on the A (root) class.

    -Blaise

    ReplyDelete
  29. We have the following requirement

    XSD A


    {
    >
    >
    >
    >
    >
    >
    >
    >
    >

    }

    {
    >
    >
    >
    >

    }

    When i generate code this happens
    >{
    >class person
    >{
    > @XmlElement(name = "homeAddress", required = true)
    > protected xsdb.AddressType homeAddress;
    >}
    >}
    but we want
    {
    >class person
    >{
    > @XmlElement(name = "HomeAddress", required = true)
    > protected xsda.AddressType HomeAddress;
    >}

    }

    Just reference the class generated in common xsd and if possible even the object factory should reference xsd a object factory so it create AddressType class again.

    like
    {
    >public xsda.AddressType createAddressType() {
    > return new AddressType();
    >}
    }
    instead of

    {
    >public xsdb.AddressType createAddressType() {
    > return new AddressType();
    >}
    }
    We just need reference and we arent essentially inheriting(extending the base.).I tried with inheritance plugin and could'nt achieve it. [please correct the mistakes if any]

    {
    >
    >
    >
    >
    >
    >
    >
    >
    > a.AddressType
    >
    >
    }

    i also worked on inheritance with child class adding nothing to base something like

    in child xsd:

    {
    >
    >
    >
    >
    >
    >
    }
    That too didnt work.Got compilation errors


    Please let me know how it can be done.Thanks for the patience and effort.Much appreciated.


    --Santhosh

    ReplyDelete
  30. Hi Santhosh,

    It appears as though content was lost in your comnment. Would you mind sending me this question through my contact me page? We can do the discussion there:
    - Contact Me

    -Blaise

    ReplyDelete
  31. Hey Blaise,

    what's the situation with Bug 341149 - Enhancement: Expand @XmlDescriminatorNode to support child elements?

    How difficult would it be or is it even possible in the current implementation to have the XmlDescriminatorNode be an arbitrary xpath expression pointing to a discriminator value? What I would need is to point a node that is not a direct child (attribute or element) of the element for which type info is needed.
    I've added a comment to the above issue describing a weird case I have.

    BTW this is a very nice feature.

    Regards,
    Tom

    ReplyDelete
    Replies
    1. Hi Tom,

      We haven't got to that enhancement yet. JSON-binding has been a big focus area for us at the moment. Here is a link to a post demonstrating how you can use an XmlAdapter to get this behaviour:
      - JAXB and Inheritance - Using XmlAdapter

      -Blaise

      Delete
  32. Hi Blaise, I've been trying to get the @XmlDiscriminatorNode/Value working with my XmlSchema. I've posted details to stackoverflow here:

    http://stackoverflow.com/questions/10339908/xmldiscriminatornode-xmlschema-namespace-and-elementformdefault-qualified/10340982#10340982

    I've stepped through the eclipselink code and seems that something strange is going on with elementFormDefault = QUALIFIED vs UNQUALIFIED.

    ReplyDelete
    Replies
    1. Hi Nick,

      This is a bug in EclipseLink JAXB (MOXy). You can track our progress on this issue using the link below. I already have a fix attached to the bug, I'll check it in once the test cases have finished running.
      - http://bugs.eclipse.org/377967

      -Blaise

      Delete
  33. Hi Blaise,
    I need to use @XmlDiscriminatorNode/Value with @XmlValue.
    I'm getting this error:
    @XmlValue is not allowed on a class that derives another class.
    but the @XmlValue is specific of a single type (other types get a list of elements).
    Can you suggest me a solution or a work around?
    Thanks very much
    Giorgio

    ReplyDelete
    Replies
    1. Hi Giorgio,

      Using @XmlValue shouldn't be an issue. Could you use the the "Contact Me" link at the top of the blog to email me the exception that you are getting? Could you also let me know the version of EclipseLink that you are using?

      -Blaise

      Delete
  34. Hi Blaise,
    can I use this approach for unmarshalling?

    ReplyDelete
    Replies
    1. Yes, the same metadata works for unmarshalling as well.

      -Blaise

      Delete
  35. I see in the example that the child classes are provided to the JAXBContext.

    JAXBContext jc = JAXBContext.newInstance(Customer.class, Address.class, PhoneNumber.class);

    Is there a way to put the classes to annotation?
    Or better, could MOXy scan the classpath for subclasses? Could use jandex for that to avoid performance drawbacks.

    ReplyDelete
    Replies
    1. You can leverage the @XmlSeeAlso annotation for this. On the ContactInfo class you would have @XmlSeeAlso({Address.class, PhoneNumber.class}). Then you could just do JAXBContext.newInstance(Customer.class).

      Delete
  36. It would be nice if one could re-use @XmlElements and apply it to a class.

    ReplyDelete