July 29, 2011

JAXB (XJC) and Nested Classes

Recently I came across a complaint on Twitter about how JAXB (XJC) generates deeply nested class structures from XML schemas.  In this post I want to first explain why JAXB does this, and second how to easily configure JAXB not to do this.


XML Schema (company.xsd)

The XML schema below the customer and employee elements both contain an element called address with different definitions:

<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema 
    targetNamespace="http://www.example.org/company" 
    xmlns="http://www.example.org/company"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    elementFormDefault="qualified"> 

    <xsd:element name="customer">
        <xsd:complexType>
            <xsd:sequence>
                <xsd:element name="address">
                    <xsd:complexType>
                        <xsd:sequence>
                            <xsd:element name="street" type="xsd:string"/>
                        </xsd:sequence>
                    </xsd:complexType>
                </xsd:element>
            </xsd:sequence>
        </xsd:complexType>
    </xsd:element>

    <xsd:element name="employee">
        <xsd:complexType>
            <xsd:sequence>
                <xsd:element name="address">
                    <xsd:complexType>
                        <xsd:sequence>
                            <xsd:element name="road" type="xsd:string"/>
                        </xsd:sequence>
                    </xsd:complexType>
                </xsd:element>
            </xsd:sequence>
        </xsd:complexType>
    </xsd:element>

</xsd:schema>

Generated Class Model

Using XJC to generate classes from this XML schema will result in the following two types.  Note how they each contain a nested class called Address.  If these Address classes were generated as top level classes they would conflict with each other, this is why by default JAXB nests these classes:

XJC Call

xjc -d out company.xsd

Customer

package org.example.company;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = {"address"})
@XmlRootElement(name = "customer")
public class Customer {

    @XmlElement(required = true)
    protected Customer.Address address;

    public Customer.Address getAddress() {
        return address;
    }

    public void setAddress(Customer.Address value) {
        this.address = value;
    }

    @XmlAccessorType(XmlAccessType.FIELD)
    @XmlType(name = "", propOrder = {"street"})
    public static class Address {

        @XmlElement(required = true)
        protected String street;

        public String getStreet() {
            return street;
        }

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

    }

}

Employee

package org.example.customer;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = {"address"})
@XmlRootElement(name = "employee")
public class Employee {

    @XmlElement(required = true)
    protected Employee.Address address;

    public Employee.Address getAddress() {
        return address;
    }

    public void setAddress(Employee.Address value) {
        this.address = value;
    }

    @XmlAccessorType(XmlAccessType.FIELD)
    @XmlType(name = "", propOrder = {"road"})
    public static class Address {

        @XmlElement(required = true)
        protected String road;

        public String getRoad() {
            return road;
        }

        public void setRoad(String value) {
            this.road = value;
        }

    }

}

Generating Top Level Types

JAXB does provide a means to easily configure the generation of all top level classes. This is done through an XML schema annotation that can be supplied inline or via an external bindings file (as shown below). Note that in the external bindings file we also need to solve the name conflict between the Address classes.

External Bindings (binding.xml)

<jaxb:bindings 
    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    xmlns:jaxb="http://java.sun.com/xml/ns/jaxb"
    version="2.1">
    <jaxb:globalBindings localScoping="toplevel"/>
    <jaxb:bindings schemaLocation="company.xsd">
        <jaxb:bindings node="//xsd:element[@name='employee']/xsd:complexType/xsd:sequence/xsd:element[@name='address']/xsd:complexType">
            <jaxb:class name="EmployeeAddress"/>
        </jaxb:bindings>
    </jaxb:bindings>
</jaxb:bindings>

XJC Call

The binding file is referenced in the XJC call as follows, and will result in the generated classes below:

xjc -d out -b binding.xml company.xsd

Customer

package org.example.company;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = {"address"})
@XmlRootElement(name = "customer")
public class Customer {

    @XmlElement(required = true)
    protected Address address;

    public Address getAddress() {
        return address;
    }

    public void setAddress(Address value) {
        this.address = value;
    }

}

Address

package org.example.company;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlType;

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = {"street"})
public class Address {

    @XmlElement(required = true)
    protected String street;

    public String getStreet() {
        return street;
    }

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

}

Employee

package org.example.company;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = {"address"})
@XmlRootElement(name = "employee")
public class Employee {

    @XmlElement(required = true)
    protected EmployeeAddress address;

    public EmployeeAddress getAddress() {
        return address;
    }

    public void setAddress(EmployeeAddress value) {
        this.address = value;
    }

}

EmployeeAddress

package org.example.company;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlType;

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = {"road"})
public class EmployeeAddress {

    @XmlElement(required = true)
    protected String road;

    public String getRoad() {
        return road;
    }

    public void setRoad(String value) {
        this.road = value;
    }

}

3 comments:

  1. I followed this advice and it worked perfectly to generate java files as top-level elements. I tried expanding on this to rename the root level element (which also worked), but I lost the @XmlRootElement annotation in the process. Is there anywhere to specify the root element in the binding XML?

    ReplyDelete
    Replies
    1. Could you post a question on Stack Overflow with what you did?

      Delete
  2. By the way, it would be cool if I could choose Stack Overflow in the "Comment as" dropdown, since that's where I located this page from.

    ReplyDelete

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