December 7, 2010

Case Insensitive Unmarshalling with JAXB

Recently I've been asked in a couple different forums how to handle case insensitive unmarshalling in JAXB.  In this post I'll discuss an approach using a StAX StreamReaderDelegate.

I am considering adding this as an official feature, and would be interested in your feed back.  I am using the following bug to track this issue:


    XML Inputs

    We want our JAXB implementation to be able to unmarshal the following XML documents to the same object model:


    <customer id="1">
        <name>Jane Doe</name>
        <address>
            <street>123 A Street</street>
        </address>
    </customer>
    

    <CUSTOMER ID="1">
        <NAME>Jane Doe</NAME>
        <ADDRESS>
            <STREET>123 A Street</STREET>
        </ADDRESS>
    </CUSTOMER>
    

    <CuStOMeR Id="1">
        <NaMe>Jane Doe</NaMe>
        <AdDrEsS>
            <StReEt>123 A Street</StReEt>
        </AdDrEsS>
    </CuStOMeR>
    


    Java Model

    In the object model we will ensure that all the resulting node names are lower case.  For this object model the default mappings give us this behavior.  For other object models we could leverage the @XmlElement and @XmlAttribute annotations to achieve this:

    package blog.caseinsensitive;
    
    import javax.xml.bind.annotation.XmlAttribute;
    import javax.xml.bind.annotation.XmlRootElement;
    
    @XmlRootElement
    public class Customer {
    
        private int id;
        private String name;
        private Address address;
    
        @XmlAttribute
        public int getId() {
            return id;
        }
    
        public void setId(int id) {
            this.id = id;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public Address getAddress() {
            return address;
        }
    
        public void setAddress(Address address) {
            this.address = address;
        }
    
    }
    

    package blog.caseinsensitive;
    
    public class Address {
    
        private String street;
    
        public String getStreet() {
            return street;
        }
    
        public void setStreet(String street) {
            this.street = street;
        }
    
    }
    

    Demo Code

    We will achieve the desired effect by filtering the XML input.  This will be done by using a StreamReaderDelegate.  This delegate lets us filter the output from an XMLStreamReader.  We will leverage this delegate to return all element and attribute names as lower case.

    package blog.caseinsensitive;
    
    import java.io.FileInputStream;
    
    import javax.xml.bind.JAXBContext;
    import javax.xml.bind.Marshaller;
    import javax.xml.bind.Unmarshaller;
    import javax.xml.stream.XMLInputFactory;
    import javax.xml.stream.XMLStreamReader;
    import javax.xml.stream.util.StreamReaderDelegate;
    
    public class Demo {
    
        public static void main(String[] args) throws Exception {
            JAXBContext jc = JAXBContext.newInstance(Customer.class);
    
            XMLInputFactory xif = XMLInputFactory.newInstance();
            XMLStreamReader xsr = xif.createXMLStreamReader(new FileInputStream("src/blog/caseinsensitive/input2.xml"));
            xsr = new MyStreamReaderDelegate(xsr);
    
            Unmarshaller unmarshaller = jc.createUnmarshaller();
            Customer customer = (Customer) unmarshaller.unmarshal(xsr);
    
            Marshaller marshaller = jc.createMarshaller();
            marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
            marshaller.marshal(customer, System.out);
        }
    
        private static class MyStreamReaderDelegate extends StreamReaderDelegate {
    
            public MyStreamReaderDelegate(XMLStreamReader xsr) {
                super(xsr);
            }
    
            @Override
            public String getAttributeLocalName(int index) {
                return super.getAttributeLocalName(index).toLowerCase();
            }
    
            @Override
            public String getLocalName() {
                return super.getLocalName().toLowerCase();
            }
    
        }
    
    }
    


    XML Output

    The following is the resulting XML, the output will match the provided JAXB mappings:

    <customer id="1">
        <name>Jane Doe</name>
        <address>
            <street>123 A Street</street>
        </address>
    </customer>
    

    11 comments:

    1. Do you know how I could go about unmarshalling a list?

      ReplyDelete
    2. Hi Sohail,

      Check out the following post:

      - JAXB & Collection Properties

      Let me know if you are looking for something different.

      -Blaise

      ReplyDelete
    3. Blaise - this is a great post. The SSWUG.org team decided to feature it on the site.

      ReplyDelete
    4. Blaise, This means that I will have to set my @XmlElement.name to be lower Cased Strings.
      is there a way to prevent this restriction and still enjoy case insensitivity?

      ReplyDelete
    5. Hi eyal,

      If you are using EclipseLink JAXB (MOXy) then you can apply two sets of metadata to your object model.

      1. The first set is with the proper case that will be used for marshalling.
      2. The second is with all lower case that will be used for unmarshalling.

      One set of metadata can be specified using annotations, and the second can be specified using MOXy's XML metadata format:

      - Extending JAXB - Representing Metadata as XML

      -Blaise

      ReplyDelete
    6. Thank You! The same what I was looking for!

      ReplyDelete
    7. If I understand correctly, the limitation of this solution, is that your property names cannot be Camel Cased?

      Just an aside, why doesn't JAXB have a case-insensitive option? Do we really think that when mapping Xml elements/attributes to class properties, that case matters? (i.e. would someone ever have 2 properties with the same name, and rely on upper/lower casing to differentiate them)?

      ReplyDelete
    8. Hi Aba,

      In this particular example the XML document would marshal with all node names as lower case. If you are using EclipseLink JAXB (MOXy) you could apply a second mapping using the external mapping document:
      - Mapping Objects to Multiple XML Schemas - Weather Example

      XML schemas are case sensitive so it makes sense for JAXB (at least by default) to be case sensitive. I have the following enhancement request open to add support for case insensitive unmarshalling. Please comment on or vote for this bug to help move it up the priority list:
      - Bug 331241 - Enhancement Request: Case insensitive unmarshalling

      -Blaise

      ReplyDelete
    9. I have spring MVC based REST webservices and I am using @RequestBody and @ResponseBody annotations. I have configured JAXB2Marshaller in Spring configuration so Spring will use this to unmarshal xml requests.

      How can I inject StreamReaderDelegate into spring configuration so that the JAXBMarshaller to achieve case insensitive unmarshalling?

      Thanks

      ReplyDelete
      Replies
      1. I'm not sure which mechanisms are available to you when creating RESTful Web Services in Spring. With Java EE and JAX-RS I would create a MessageBodyReader to hook in the necessary code. For an example MessageBodyReader see:
        - MOXy as Your JAX-RS JSON Provider - Server Side

        -Blaise

        Delete
    10. Your JAXB blog posts are an endless supply of good information. This really helped me solve a problem I had. Works great, thanks!

      ReplyDelete

    Note: Only a member of this blog may post a comment.