JSON Support in Axis2

Introduction

Update: This documentation represents early forms of JSON conventions, Badgerfish and Mapped. GSON support was added a few years later. Moshi support is now included as an alternative to GSON. For users of JSON seeking modern features, see the JSON Support Guide.. For users of JSON and Spring Boot, see the sample application in the JSON and Spring Boot User's Guide.

This document explains the JSON support implementation in Axis2. It includes an introduction to JSON, an outline as to why JSON support is useful to Axis2 and how it should be used. This document also provides details on test cases and samples.

What is JSON?

JSON (Java Script Object Notation) is another data exchangeable format like XML, but more lightweight and easily readable. It is based on a subset of the JavaScript language. Therefore, JavaScript can understand JSON, and it can make JavaScript objects by using JSON strings. JSON is based on key-value pairs and it uses colons to separate keys and values. JSON doesn't use end tags, and it uses braces (curly brackets) to enclose JSON Objects.

e.g. <root><test>json object</test></root> == {{json object}}

When it comes to converting XML to JSON and vice versa, there are two major conventions, one named "Badgerfish" and the other, Mapped. The main difference between these two conventions exists in the way they map XML namespaces into JSON.

e.g. <xsl:root xmlns:xsl="http://foo.com"><data>my json string</data></xsl:root>

This XML string can be converted into JSON as follows.

Using Badgerfish

{"xsl:root":{"@xmlns":{"xsl":"http://foo.com"},"data":{"$":"my json string"}}}

Using Mapped

If we use the namespace mapping as http://foo.com -> foo

{"foo.root":{"data":"my json string"}}

Why JSON Support for Axis2?

Apache Axis2 is a Web services stack that delivers incoming messages into target applications. In most cases, these messages are SOAP messages. In addition, it is also possible to send REST messages through Axis2. Both types of messages use XML as their data exchangeable format. So if we can use XML as a format, why use JSON as another format?

There are many advantages of implementing JSON support in Axis2. Mainly, it helps the JavaScript users (services and clients written in JavaScript) to deal with Axis2. When the service or the client is in JavaScript, it can use the JSON string and directly build JavaScript objects to retrieve information, without having to build the object model (OMElement in Axis2). Also, JavaScript services can return the response through Axis2, just as a JSON string can be shipped in a JSONDataSource.

Other than for that, there are some extra advantages of using JSON in comparison to XML. Although the conversation XML or JSON? is still a hot topic, many people accept the fact that JSON can be passed and built more easily by machines than XML.

How to use JSON in Axis2

At the moment JSON doesn't have a standard and unique content type. application/json (this is the content type which is approved in the JSON RFC), text/javascript and text/json are some of the commonly used content types for JSON. Fortunately, in Axis2, the user has the freedom of specifying the content type to use.

Configuring axis2.xml

First of all, you need to map the appropriate message formatters and builders to the content type you are using in the axis2.xml file. This applies both the to client side and the server side.

E.g., if you are using the Mapped convention with the content type application/json, add the following declaration:

    <messageFormatters>        
        <messageFormatter contentType="application/json"
                          class="org.apache.axis2.json.JSONMessageFormatter"/>
        <!-- more message formatters -->
    </messageFormatters>   

    <messageBuilders>
        <messageBuilder contentType="application/json"
                        class="org.apache.axis2.json.JSONOMBuilder"/>
        <!-- more message builders -->
    </messageBuilders>

If you are using the Badgerfish convention with the content type text/javascript, add:

    <messageFormatters>        
        <messageFormatter contentType="text/javascript"
                          class="org.apache.axis2.json.JSONBadgerfishMessageFormatter">
        <!-- more message formatters -->
    </messageFormatters> 

    <messageBuilders>
        <messageBuilder contentType="text/javascript"
                        class="org.apache.axis2.json.JSONBadgerfishOMBuilder"/>
        <!-- more message builders -->
    </messageBuilders>

Client-side configuration

On the client side, make the ConfigurationContext by reading the axis2.xml in which the correct mappings are given.

e.g.

        File configFile = new File("test-resources/axis2.xml");
        configurationContext = ConfigurationContextFactory
                        .createConfigurationContextFromFileSystem(null, configFile.getAbsolutePath());
        ..........        
        ServiceClient sender = new ServiceClient(configurationContext, null);

Set the MESSAGE_TYPE option with exactly the same content type you used in the axis2.xml.

e.g. If you use the content type application/json,

        Options options = new Options();        
        options.setProperty(Constants.Configuration.MESSAGE_TYPE, "application/json");
        //more options
        //...................        

        ServiceClient sender = new ServiceClient(configurationContext, null);        
        sender.setOptions(options);

If you are sending a request to a remote service, you have to know the exact JSON content type that is used by that service, and you have to use that content type in your client as well.

HTTP POST is used as the default method to send JSON messages through Axis2, if the HTTP method is not explicitly set by the user. But if you want to send JSON in HTTP GET method as a parameter, you can do that by just setting an option on the client side.

e.g.

options.setProperty(Constants.Configuration.HTTP_METHOD, Constants.Configuration.HTTP_METHOD_GET);

Here, the Axis2 receiving side (JSONOMBuilder) builds the OMElement by reading the JSON string which is sent as a parameter. The request can be made even through the browser.

e.g. Sample JSON request through HTTP GET. The JSON message is encoded and sent.

GET /axis2/services/EchoXMLService/echoOM?query=%7B%22echoOM%22:%7B%22data%22:%5B%22my%20json%20string%22,%22my%20second%20json%20string%22%5D%7D%7D HTTP/1.1

Server-side configuration

Since Badgerfish defines a 1-to-1 transformation between JSON and XML, no additional configuration is required on the server side if that convention is used. Any service deployed into Axis2 will work out of the box.

On the other hand, if the Mapped JSON convention is used, then Axis2 needs to know the mappings between XML namespaces and JSON "namespaces" in order to translate messages from JSON into XML representations and vice-versa. To use the Mapped convention with a service deployed into Axis2, add a xmlToJsonNamespaceMap property with these mappings to the services.xml file for that service, as shown in the following example:

<service name="...">
    ...
    <parameter name="xmlToJsonNamespaceMap">
        <mappings>
            <mapping xmlNamespace="http://example.org/foo" jsonNamespace=""/>
            <mapping xmlNamespace="http://example.org/bar" jsonNamespace="bar"/>
        </mappings>
    </parameter>
    ...
</service>

How the JSON implementation works - Architecture

Introduction

The Axis2 architecture is based on the assumption that any message flowing through the Axis2 runtime is representable as a SOAP infoset, i.e. as XML wrapped in a SOAP envelope. Conceptually, the two message builders JSONOMBuilder and JSONBadgerfishOMBuilder convert incoming messages from JSON to XML and the two message formatters JSONMessageFormatter and JSONBadgerfishMessageFormatter convert outgoing messages from XML to JSON. Axis2 doesn't implement its own JSON parser and serializer, and instead relies on Jettison to do the JSON<->XML conversions.

On the server side the XML for an incoming message is typically converted to Java objects by a databinding (such as ADB or JAX-WS) before the invocation of the service implementation. In the same way, the Java object returned by the service implementation is converted to XML. In the case we are interested in, that XML is then converted by the message formatters to JSON. The usage of an intermediate XML representation is the reason why JSON can be enabled on any service deployed in Axis2.

It is important to note that the explanation given in the previous two paragraphs is only valid from a conceptual point of view. The actual processing model is more complicated. In the next two sections we will explain in detail how Axis2 processes incoming and outgoind JSON messages.

Processing of incoming JSON messages

Axis2 relies on Apache Axiom as its XML object model. Although Axiom has a DOM like API, it also has several advanced features that enable Axis2 to avoid building a complete object model representation of the XML message. This is important for performance reasons and distinguishes Axis2 from previous generation SOAP stacks. To leverage these features, the JSON message builders create a SOAP envelope the body of which contains a single OMSourcedElement.

An OMSourcedElement is a special kind of OMElement that wraps an arbitrary Java object that can be converted to XML in a well defined way. More precisely, the Java object as well as the logic to convert the object to XML are encapsulated in an OMDataSource instance and it is that OMDataSource instance that is used to create the OMSourcedElement. For JSON, the OMDataSource implementation is JSONDataSource or JSONBadgerfishDataSource, depending on the convention being used. The base class (AbstractJSONDataSource) of these two classes actually contains the code that invokes Jettison to perform the JSON to XML conversion.

An OMSourcedElement still behaves like a normal OMElement. In particular, if the element is accessed using DOM like methods, then Axiom will convert the data encapsulated by the OMDataSource on the fly to an object model representation. This process is called expansion of the OMSourcedElement. However, the OMDataSource API is designed such that the conversion to XML is always done using a streaming API: either the OMDataSource produces an XMLStreamReader instance from which the XML representation can be read (this is the case for JSON and the XMLStreamReader implementation is actually provided by Jettison) or it serializes the XML representation to an XMLStreamWriter. Because of this, expansion of the OMSourcedElement is often not necessary, so that the overhead of creating an object model representation can usually be avoided. E.g. a databinding will typically consume the message by requesting an XMLStreamReader for the element in the SOAP body, and this doesn't require expansion of the OMSourcedElement. In this case, the databinding pulls the XML data almost directly from the underlying Jettison XMLStreamReader and no additional Axiom objects are created.

Actually here again, things are slightly more complicated because in order to dispatch to the right operation, Axis2 needs to determine the name of the element in the body. Since the name is not known in advance, that operation requires expansion of the OMSourcedElement. However, at this point none of the children of the OMSourcedElement will be built. Fortunately the databindings generally request the XMLStreamReader with caching turned off, so that the child nodes will never be built. Therefore the conclusion of the previous paragraph remains valid: processing the message with a databinding will not create a complete object model representation of the XML.

Usage of an OMSourcedElement also solves another architectural challenge posed by the Mapped JSON convention: the JSON payload can only be converted to XML if the namespace mappings are known. Since they are defined per service, they are only known after the incoming message has been dispatched and the target service has been identified. This typically occurs in RequestURIBasedDispatcher, which is executed after the message builder. This means that JSONOMBuilder cannot actually perform the conversion. Usage of an OMSourcedElement avoids this issue because the conversion is done lazily when the OMSourcedElement is first accessed, and this occurs after RequestURIBasedDispatcher has been executed.

Another advantage of using OMSourcedElement is that a JSON aware service could directly process the JSON payload without going through the JSON to XML conversion. That is possible because the OMDataSource simply keeps a reference to the JSON payload and this reference is accessible to JSON aware code.

Processing of outgoing messages

For outgoing messages, the two JSON message formatters JSONMessageFormatter and JSONBadgerfishMessageFormatter use Jettision to create an appropriate XMLStreamWriter and then request Axiom to serialize the body element to that XMLStreamWriter. If a databinding is used, then the body element will typically be an OMSourcedElement with an OMDataSource implementation specific to that databinding. OMSourcedElement will delegate the serialization request to the appropriate method defined by OMDataSource. This means that the databinding code directly writes to the XMLStreamWriter instance provided by Jettision, without building an intermediate XML object model.

Before doing this, the JSON message formatters actually check if the element is an OMSourcedElement backed by a corresponding JSON OMDataSource implementation. If that is the case, then they will extract the JSON payload and directly write it to the output stream. This allows JSON aware services to bypass the XML to JSON conversion entirely.

Tests and Samples

Integration Test

The JSON integration test is available under test in the json module of Axis2. It uses the SimpleHTTPServer to deploy the service. A simple echo service is used to return the incoming OMSourcedElement object, which contains the JSONDataSource. There are two test cases for two different conventions and another one test case to send the request in GET.

Yahoo-JSON Sample

This sample is available in the samples module of Axis2. It is a client which calls the Yahoo search API using the GET method, with the parameter output=json. The Yahoo search service sends the response as a formatted JSON string with the content type text/javascript. This content type is mapped with the JSONOMBuilder in the axis2.xml. All the results are shown in a GUI. To run the sample, execute the ant script.

These two applications provide good examples of using JSON within Axis2. By reviewing these samples, you will be able to better understand Axis2's JSON support implementation.

Enabling mapped JSON on the ADB quickstart sample

To illustrate how JSON can be enabled on an existing service deployed in Axis2, we will use the ADB stock quote service sample from the Quick Start Guide. The code for this sample can be found in the samples/quickstartadb folder in the binary distribution.

Only a few steps are necessary to enable JSON (using the Mapped convention) on that service:

  1. Configure the JSON message builders and formatters in conf/axis2.xml. Add the following element to the messageFormatters:

    <messageFormatter contentType="application/json"
                      class="org.apache.axis2.json.JSONMessageFormatter"/>
    

    Also add the following element to the messageBuilders:

    <messageBuilder contentType="application/json"
                    class="org.apache.axis2.json.JSONOMBuilder"/>
    
  2. Edit the services.xml for the stock quote service and add the following configuration:

    <parameter name="xmlToJsonNamespaceMap">
        <mappings>
            <mapping xmlNamespace="http://quickstart.samples/xsd" jsonNamespace=""/>
        </mappings>
    </parameter>
    

    The services.xml file can be found under samples/quickstartadb/resources/META-INF.

  3. Build and deploy the service by executing the ant script in samples/quickstartadb and then start the Axis2 server using bin/axis2server.sh or bin/axis2server.bat.

That's it; the stock quote service can now be invoked using JSON. This can be tested using the well known curl tool:

curl -H 'Content-Type: application/json' -d '{"getPrice":{"symbol":"IBM"}}' http://localhost:8080/axis2/services/StockQuoteService

This will give the following result:

{"getPriceResponse":{"return":42}}