First of all, JAXB is a Java standard for binding XML schemas to Java classes. It allows you to convert Java objects to XML documents, and vice versa, based on JAXB annotations on the corresponding Java classes. JAXB doesn't cover JSON but there are libraries that allow you to convert Java objects to JSON (and vice versa) based on the very same JAXB annotations that are used for defining XML bindings. One such library is Jersey's JSON library (jersey-json) which internally uses the Jackson library.
As you'll see in the following, JAXB can also be used together with immutable domain or resource models based on Scala case classes. There's no need to pollute them with getters and setters or Java collections from the
java.util
package. Necessary conversions from Scala collections or other type constructors (such as Option
, for example) to Java types supported by JAXB can be defined externally to the annotated model (and reused). At the end of this blog post, I'll also show some examples how to develop JAXB-based XML and JSON APIs with the Play Framework.Model
In the following, I'll use a model that consists of the singlecase class Person(fullname: String, username: Option[String], age: Int)
. To define Person
as root element in the XML schema, the following JAXB annotations should be added.@XmlRootElement
makes Person
a root element in the XML schema and @XmlAccessorType(XmlAccessType.FIELD)
instructs JAXB to access fields directly instead of using getters and setters. But before we can use the Person
class with JAXB a few additional things need to be done.- A no-arg constructor or a static no-arg factory method must be provided, otherwise, JAXB doesn't know how to create
Person
instances. In our example we'll use a no-arg constructor. - A person's
fullname
should be mandatory in the corresponding XML schema. This can be achieved by placing an@XmlElement(required=true)
annotation on the field corresponding to thefullname
parameter. - A person's
username
should be an optionalString
in the corresponding XML schema i.e. theusername
element of the complexPerson
type should have an XML attributeminOccurs="0"
. Furthermore, it should be avoided thatscala.Option
appears as complex type in the XML schema. This can be achieved by providing a type adapter fromOption[String]
toString
via the JAXB@XmlJavaTypeAdapter
annotation.
We can implement the above requirements by defining and annotating the
Person
class as follows:Let's dissect the above code a bit:
- The no-arg constructor on the
Person
class is only needed by JAXB and should therefore be declared private so that it cannot be accessed elsewhere in the application code (unless you're using reflection like JAXB does). - Placing JAXB annotations on fields of a case class is a bit tricky. When writing a case class, usually only case class parameters are defined but not fields directly. The Scala compiler then generates the corresponding fields in the resulting .class file. Annotations that are placed on case class parameters are not copied to their corresponding fields, by default. To instruct the Scala compiler to copy these annotations, the Scala
@field
annotation must be used in addition. This is done in the custom annotation type definitionsxmlElement
andxmlTypeAdapter
. They can be used in the same way as their dependent annotation typesXmlElement
andXmlJavaTypeAdapter
, respectively. Placing the custom@xmlElement
annotation on thefullname
parameter will cause the Scala compiler to copy the dependent@XmlElement
annotation (a JAXB annotation) to the generatedfullname
field where it can be finally processed by JAXB. - To convert between
Option[String]
(on Scala side) andString
(used by JAXB on XML schema side) we implement a JAXB type adapter (interfaceXmlAdapter
). The above example defines a genericOptionAdapter
(that can also be reused elsewhere) and a concreteStringOptionAdapter
used for the optionalusername
parameter. Please note that annotating theusername
parameter with@xmlTypeAdapter(classOf[OptionAdapter[String]])
is not sufficient because JAXB will not be able to inferString
as the target type (JAXB uses reflection) and will useObject
instead (resulting in an XMLanyType
in the corresponding XML schema). Type adapters can also be used to convert between Scala and Java collection types. Since JAXB can only handle Java collection types you'll need to use type adapters should you want to use Scala collection types in your case classes (and you really should). You can find an example here.
We're now ready to use the
Person
class to generate an XML schema and to convert Person
objects to and from XML or JSON. Please note that the following code examples require JAXB version 2.2.4u2 or higher, otherwise the OptionAdapter
won't work properly. The reason is JAXB issue 415. Either use JDK 7u4 or higher which already includes this version or install the required JAXB version manually. The following will write an XML schema, generated from the Person
class, to stdout:The result is:
Marshalling a
Person
object to XML can be done withwhich prints
Unmarshalling creates a
Person
object from XML.We have implemented
StringOptionAdapter
in a way that an empty <username/>
element or <username></username>
in personXml1
would also yield None
on Scala side. Creating JSON from Person
objects can be done with the JSONJAXBContext
from Jersey's JSON library.which prints the following to stdout:
Unmarshalling can be done with the
context.createJSONUnmarshaller.unmarshalFromJSON
method. The JSONConfiguration
object provides a number of configuration options that determine how JSON is rendered and parsed. Refer to the official documentation for details.Play and JAXB
This section shows some examples how to develop JAXB-based XML and JSON APIs with the Play Framework 2.0. It is based on JAXB-specific body parsers and type class instances defined in traitJaxbSupport
which is part of the event-sourcing example application (Play-specific work is currently done on the play branch). You can reuse this trait in other applications as is, there are no dependencies to the rest of the project (update: except to SysError
). To enable JAXB-based XML and JSON processing for a Play web application, add JaxbSupport
to a controller object as follows:An implicit
JSONJAXBContext
must be in scope for both XML and JSON processing. For XML processing alone, it is sufficient to have an implicit JAXBContext.XML and JSON Parsing
JaxbSupport
provides Play-specific body parsers that convert XML or JSON request body to instances of JAXB-annotated classes. The following action uses a JAXB body parser that expect an XML body and tries to convert it to a Person
instance (using a JAXB unmarshaller). If the unmarshalling fails or the request
Content-Type
is other than text/xml
or application/xml
, a 400
status code (bad request) is returned. Converting a JSON body to a Person
instance can be done with the jaxb.parse.json
body parser.If the body parser should be chosen at runtime depending on the
Content-Type
header, use the dynamic jaxb.parse
body parser. The following action is able to process both XML and JSON bodies and convert them to a Person
instance.JaxbSupport
also implements the following related body parsers jaxb.parse.xml(maxLength: Int)
andjaxb.parse.json(maxLength: Int)
jaxb.parse(maxLength: Int)
andjaxb.parse(maxLength: Int)
jaxb.parse.tolerantXml
andjaxb.parse.tolerantJson
jaxb.parse.tolerantXml(maxLength: Int)
andjaxb.parse.tolerantJson(maxLength: Int)
XML and JSON Rendering
For rendering XML and JSON, JaxbSupport provides the wrapper classesJaxbXml
, JaxbJson
and Jaxb
. The following action renders an XML response from a Person
object (using a JAXB marshaller):whereas
renders a JSON response from a
Person
object. If you want to do content negotiation based on the Accept
request header, use the Jaxb
wrapper.Jaxb requires an implicit
request
in context for obtaining the Accept
request header. If the Accept
MIME type is application/xml
or text/xml
an XML representation is returned, if it is application/json
a JSON representation is returned. Further JaxbSupport
application examples can be found here.
Thanks for the article.
ReplyDeleteWhat do you think of Jerkson? It's a wrapper around Jackson, and then I've also made a pull request to use default parameters if no equivalent is provided in the JSON: https://github.com/codahale/jerkson/pull/42
I've never used Jerkson, so I cannot make a comparison. However, if I only wanted to build a JSON API for a Scala web app, I'd probably follow a Jerkson-based approach first or use Play's JSON support on top of Jerkson before following a JAXB-based approach. I gave the JAXB-based approach a try because I wanted to explore if it's possible to generate XML and JSON APIs from a single binding definition (JAXB annotations). I'm still experimenting ...
DeleteThis comment has been removed by the author.
DeleteThe move to play 2 is looking great!!!
ReplyDeleteFYI to download the play version>>>>> git clone https://github.com/krasserm/eventsourcing-example.git --branch play
> You can reuse this trait in other applications as is, there are no dependencies to the rest of the project.
ReplyDeleteJust want to comment that there is a dependency on Syserror, which is just....
@XmlRootElement(name = "sys-error")
@XmlAccessorType(XmlAccessType.FIELD)
case class SysError(message: String) {
def this() = this(null)
}
object SysError {
val NotFound = SysError("Not Found")
}
Good catch, thanks. Updated the blog.
DeleteHi Martin,
ReplyDeleteThanks for this great post and the accompanying github repo. They were literally the only resources on JAXB in Scala that I could find and I wouldn't have got anywhere in my experimentations without them.
I ran into a few corner cases around Options and Lists and decided to blog about them.
I hope that's okay!