46

I'm using JAXB to read and write XML. What I want is to use a base JAXB class for marshalling and an inherited JAXB class for unmarshalling. This is to allow a sender Java application to send XML to another receiver Java application. The sender and receiver will share a common JAXB library. I want the receiver to unmarshall the XML into a receiver specific JAXB class which extends the generic JAXB class.

Example:

This is the common JAXB class which is used by the sender.

@XmlRootElement(name="person")
public class Person {
    public String name;
    public int age;
}

This is the receiver specific JAXB class used when unmarshalling the XML. The receiver class has logic specific to the receiver application.

@XmlRootElement(name="person")
public class ReceiverPerson extends Person {
    public doReceiverSpecificStuff() ...
}

Marshalling works as expected. The problem is with unmarshalling, it still unmarshals to Person despite the JAXBContext using the package name of the subclassed ReceiverPerson.

JAXBContext jaxbContext = JAXBContext.newInstance(package name of ReceiverPerson);

What I want is to unmarshall to ReceiverPerson. The only way I've been able to do this is to remove @XmlRootElement from Person. Unfortunately doing this prevents Person from being marshaled. It's as if JAXB starts at the base class and works its way down until it finds the first @XmlRootElement with the appropriate name. I've tried adding a createPerson() method which returns ReceiverPerson to ObjectFactory but that doesn't help.

Steve Kuo
  • 61,876
  • 75
  • 195
  • 257

6 Answers6

21

The following snippet is a method of a Junit 4 test with a green light:

@Test
public void testUnmarshallFromParentToChild() throws JAXBException {
  Person person = new Person();
  int age = 30;
  String name = "Foo";
  person.name = name;
  person.age= age;

  // Marshalling
  JAXBContext context = JAXBContext.newInstance(person.getClass());
  Marshaller marshaller = context.createMarshaller();

  StringWriter writer = new StringWriter();
  marshaller.marshal(person, writer);

  String outString = writer.toString();

  assertTrue(outString.contains("</person"));

  // Unmarshalling
  context = JAXBContext.newInstance(Person.class, RecieverPerson.class);
  Unmarshaller unmarshaller = context.createUnmarshaller();
  StringReader reader = new StringReader(outString);
  RecieverPerson reciever = (RecieverPerson)unmarshaller.unmarshal(reader);

  assertEquals(name, reciever.name);
  assertEquals(age, reciever.age);
}

The important part is the use of the JAXBContext.newInstance(Class... classesToBeBound) method for the unmarshalling context:

 context = JAXBContext.newInstance(Person.class, RecieverPerson.class);

With this call, JAXB will compute a reference closure on the classes specified and will recognize RecieverPerson. The test passes. And if you change the parameters order, you'll get a java.lang.ClassCastException (so they must be passed in this order).

Pascal Thivent
  • 562,542
  • 136
  • 1,062
  • 1,124
  • This one actually works the best, no need for adapters if you can use JAXB annotations for the classes. – alex440 Jan 06 '20 at 15:00
20

You're using JAXB 2.0 right? (since JDK6)

There is a class:

javax.xml.bind.annotation.adapters.XmlAdapter<ValueType,BoundType>

which one can subclass, and override following methods:

public abstract BoundType unmarshal(ValueType v) throws Exception;
public abstract ValueType marshal(BoundType v) throws Exception;

Example:

public class YourNiceAdapter
        extends XmlAdapter<ReceiverPerson,Person>{

    @Override public Person unmarshal(ReceiverPerson v){
        return v;
    }
    @Override public ReceiverPerson marshal(Person v){
        return new ReceiverPerson(v); // you must provide such c-tor
    }
}

Usage is done by as following:

@Your_favorite_JAXB_Annotations_Go_Here
class SomeClass{
    @XmlJavaTypeAdapter(YourNiceAdapter.class)
    Person hello; // field to unmarshal
}

I'm pretty sure, by using this concept you can control the marshalling/unmarshalling process by yourself (including the choice the correct [sub|super]type to construct).

ivan_ivanovich_ivanoff
  • 19,113
  • 27
  • 81
  • 100
  • I tried this and it doesn't work. No matter what I try, ObjectFactory or my XmlAdapter never gets called. From what I've read Sun's JAXB resolves through static class references. It appears that Glassfish's implementation has more promise https://jaxb.dev.java.net/guide/Adding_behaviors.html – Steve Kuo Mar 18 '09 at 00:48
  • XmlJavaAdapters are suited to make non-JAXB-annotated classes usable with JAXB. Because Person and ReceiverPerson (and/or their superlass, if any) have JAXB annotations, it doesn't work. You need Person and ReceiverPerson without JAXB, plus adapters for both. – ivan_ivanovich_ivanoff Mar 19 '09 at 01:09
  • Have you tried swapping types for marshall|unmarshall? [...] extends XmlAdapter [...] @Override public ReceiverPerson unmarshal(Person v){ return new ReceiverPerson(v); } @Override public Person marshal(ReceiverPerson v){ return v; } – ivan_ivanovich_ivanoff Mar 19 '09 at 01:14
  • Please ignore the first comment ;) It MUST work also on JAXB-annotated types, why not? (Well I coudn't find something in the spec). Just replace "extends XmlAdapter" with "extends XmlAdapter" and the words marshal<->unmarshall, as said in second comment – ivan_ivanovich_ivanoff Mar 19 '09 at 01:20
  • May be you could just update your answer with the correct use? – Robert Apr 28 '11 at 17:32
  • 3
    Because Stackoverflow automatically awards the response with the most votes as the answer when then question has a bounty. – Steve Kuo Dec 07 '11 at 21:23
13

Subclass Person twice, once for receiver and once for sender, and only put the XmlRootElement on these subclassses (leaving the superclass, Person, without an XmlRootElement). Note that sender and receiver both share the same JAXB base classes.

@XmlRootElement(name="person")
public class ReceiverPerson extends Person {
  // receiver specific code
}

@XmlRootElement(name="person")
public class SenderPerson extends Person {
  // sender specific code (if any)
}

// note: no @XmlRootElement here
public class Person {
  // data model + jaxb annotations here
}

[tested and confirmed to work with JAXB]. It circumvents the problem you note, when multiple classes in the inheritance hierarchy have the XmlRootElement annotation.

This is arguably also a neater and more OO approach, because it separates out the common data model, so it's not a "workaround" at all.

13ren
  • 11,887
  • 9
  • 47
  • 64
  • It looks as best solution but doesn't work for me, perhaps because my case is slightly complicated. I need to convert class instances of 'Group' class that contains both list of SenderPerson and ReceiverPerson. something like that @XmlRootElement public class Group { public List senders; public List receivers; } – Igor Grinfeld Jun 02 '13 at 19:01
7

Create a custom ObjectFactory to instantiate the desired class during unmarshalling. Example:

JAXBContext context = JAXBContext.newInstance("com.whatever.mypackage");
Unmarshaller unmarshaller = context.createUnmarshaller();
unmarshaller.setProperty("com.sun.xml.internal.bind.ObjectFactory", new ReceiverPersonObjectFactory());
return unmarshaller;

public class ReceiverPersonObjectFactory extends ObjectFactory {
    public Person createPerson() {
        return new ReceiverPerson();
    }
}
vocaro
  • 2,779
  • 2
  • 23
  • 16
  • That seems to me like a very clever way as it requires to create each object only once - compared to the XmlAdpater solution where you first create the bound type class instance and then copy all attributes to the value class instance. But the question is: Does this work also with JREs different to Sun/Oracle like OpenJDK? – Robert Apr 28 '11 at 17:37
  • +1 for being the only solution to me. I'm trying to use type substitution for subclassing (implClass) in JAXB 2.* in a xsd file and the classes unmarshaled are generated ones other than the one specified in implClass. vocaro's answer resolve the problem! – Tatera Aug 05 '15 at 08:41
  • 1
    Minor update for the case when JAXB is an external implementation (not one from SDK): the property is called `"com.sun.xml.bind.ObjectFactory"`. – dma_k Mar 02 '16 at 14:27
3

I am not sure why you would want to do this... it doesn't seem all that safe to me.

Consider what would happen in ReceiverPerson has additional instance variables... then you would wind up with (I guess) those variables being null, 0, or false... and what if null is not allowed or the number must be greater than 0?

I think what you probably want to do is read in the Person and then construct a new ReceiverPerson from that (probably provide a constructor that takes a Person).

TofuBeer
  • 60,850
  • 18
  • 118
  • 163
  • ReceiverPerson does not have an additional variables. Its purpose is to do receiver specific stuff. – Steve Kuo Mar 16 '09 at 17:10
  • You are still going to have to, at some level, create it as a different class... and the normal way of doing that is via constructor that takes the type you want to convert from. Also, just because that is the case now doesn't mean you won't add vars in the future. – TofuBeer Mar 16 '09 at 17:13
  • 3
    Yikes, that is way to much speculation about what a class might do in the future. If it's good enough for today then it's good enough, and you can always refactor later... – Amber Shah Apr 13 '10 at 19:47
  • 2
    Consumers of public APIs love when they change... not. Refactoring is fine for internal things, but anything that is public it is not. The whole point of refactoring is to change the way things work behind the scenes without breaking things. If you are changing the public API you are not refactoring, you are re-writing. – TofuBeer Apr 13 '10 at 20:22
-1

Since you really have two separate apps, compile them with different versions of the class "Person" - with the receiver app not having @XmlRootElement(name="person") on Person. Not only is this ugly, but it defeats the maintainability you wanted from using the same definition of Person for both sender and receiver. Its one redeeming feature is that it works.

13ren
  • 11,887
  • 9
  • 47
  • 64
  • I want both apps to share the common base JAXB classes. – Steve Kuo Mar 18 '09 at 00:49
  • @Steve: but my second ("neat way") solution does share common base JAXB classes... it looks like you only read the first paragraph, and I wasn't clear that I was giving two solutions. I'll edit. – 13ren Mar 18 '09 at 09:09
  • I separated "neat way" into a new answer. – 13ren Mar 18 '09 at 09:19