Export to GitHub

rest4j - Mappings.wiki


Introduction

Rest4j is a powerful tool for separating concerns related to REST API from your business logic. It contains means of mapping internal business objects into their external JSON representation. The mapping is specified by special attributes in the API description XML and user-written code.

The following is the description of the different tactics used to decouple external representation from Java classes used in your code.

Renaming Properties

You can specify a different property name in the API description XML: <!-- property renaming. The property is called "petWeight" in your Java class, but JSON will use the name "weight" --> <simple name="weight" type="number" prop="petWeight" nullable="true"/>

By default, JSON fields are named after the corresponding Java properties. You can specify different Java property name with the 'prop' attribute.

Field Mappers

By default, POJO getters and setters are used. Field mappers specify alternative getter and setter methods for JSON fields. The power of field mappers is the separation of this getter/setter logic from your main code. Here is the example of using filed mappers, in petapi.xml:

<model name="Pet" class="com.pets.api.Pet" field-mapper="pets"> <fields> <complex name="relations" collection="array" type="PetRelation" mapping-method="petRelations" nullable="false" default="empty"/> . . . </fields> </model>

Note the field-mapper and mapping-method attributes. Here, there is no property named 'relations' in the com.pets.api.Pet class. Instead, this property is read by the PetMapping.petRelations(Pet pet) method and is written by the PetMapping.petRelations(Pet pet, List<PetRelation>):

public class PetMapping { public List<PetRelation> petRelations(Pet pet) { // create and return the PetRelation list } public void petRelations(Pet pet, List<PetRelation> list) { // set the petRelations field } }

For this to work, you need to register the 'pets' field mapper in your Spring context:

<bean id="petsMapper" class="com.pet.api.PetMapping"/>

Field mappers are found using the ServiceProvider interface. The Spring's implementation of the ServiceProvider finds object by their bean id, adding a suffix configured in the !APIFactoryBean. Here the suffix is "Mapper".

Converters

Converters can be applied to several API fields. Example:

<simple name="url" type="string" converter="urlToString"/>

Again, to make it happen, you have to register the converter in your Spring context:

<bean id="urlToStringConverter" class="com.pets.api.UrlToStringCvt"/>

Here the suffix "Converter" should be configured in the !APIFactoryBean. Converters are classes that implement com.rest4j.Converter interface and contain marshal and unmarshal methods that convert a field value to and from JSON type system.

Field Filters

While converters convert values, field mappers convert (or even remove) fields. Field filters are applied globally to all the JSON fields that are marshalled or unmarshalled. Like converters, filters contain two methods: marshal and unmarshal, but the methods receive some additional information about the JSON field along with the field value itself. You register filters with the !APIFactoryBean in your Spring context:

<bean id="api" class="com.rest4j.spring.APIFactoryBean"> ... <property name="fieldFilters"> <list> <bean class+="my.bean.filter"/> </list> </property> </bean>

Filed filters implement the com.rest4j.FieldFilter interface.

Constant Fields

You can set simple JSON fields to a constant value:

<simple name='integer' type='number' value='555' nullable="false"/>

The field will alway have value '555' on the JSON output and checked to be '555' on input.

Object Factories and Polymorphism

JSON does not support polymorphism the way it exists in object-oriented languages. Instead, you should invent some way to distinguish different object types when reading JSON data. One such trick is using a syntetic discriminator field, say 'type'. When marshalling a JSON object, you can create a field mapper that detects the object's type and outputs the corresponding 'type' value. Assume there is an abstract Shape class and two subclasses:

``` public abstract class Shape { int x, y;

public int getX() { return x; } public void setX(int value) { this.x = value; } public int getY() { return y; } public void setY(int value) { this.y = value; } }

public class Circle extends Shape { int radius;

public int getRadius() { return radius; } public void setRadius(int r) { this.radius = r; } }

public class Rectangle extends Shape { int width, height;

pubilc int getWidth() { return width; } public void setWidth(int w) { this.width = w; } public int getHeight() { return height; } public void setHeight(int h) { this.height = h; } } ```

Here is out Shape model definition:

<model name="Shape" class="Shape" field-mapper="shape"> <fields> <simple name="type" type="string" nullable="false" mapping-method="type"> <values> <value>CIRCLE</value> <value>RECT</value> </values> </simple> <simple name="x" type="number"/> <simple name="y" type="number"/> <simple name="radius" type="number" optional="true"/> <simple name="width" type="number" optional="true"/> <simple name="height" type="number" optional="true"/> </fields> </model>

The 'type' field is defined with its mapping method. Here is the possible implementation of the method getter an setter:

public class ShapeMapper { public String type(Shape shape) { if (shape instanceof Rectangle) return "RECT"; return "CIRCLE"; } public void type(Shape shape, String value) { } }

As it can be seen, there is a slight problem in the setter: the field cannot be set on an existing Shape object, because it determines the object's type and should be taken into account during the object's creation. To achieve this, we will use our custom ObjectFactory:

``` public class MyObjectFactory implements ObjectFactory { @Override public Object createInstance(String modelName, Class clz, JSONObject object, ObjectFactoryChain next) throws JSONException, ApiException { if (modelName.equals("Shape")) { if ("RECT".equals(object.getString("type")) return new Rectangle(); else return new Circle(); } return next.createInstance(modelName, clz, object); } }

```

The object factory intercepts creation of "Shape" objects and delegates creation of other objects to the default factory.

Here again, we need to register out object factory with the APIFactoryBean:

<bean id="api" class="com.rest4j.spring.APIFactoryBean"> ... <property name="objectFactories"> <list> <bean class="my.package.MyObjectFactory"/> </list> </property> </bean>