JSON (JavaScript Object Notation) is a popular format to represent and exchange data especially for modern web-clients. For mapping Java objects to JSON and vice-versa there is no official standard API. We use the established and powerful open-source solution Jackson. Due to problems with the wiki of fasterxml you should try this alternative link: Jackson/AltLink.
In order to avoid polluting business objects with proprietary Jackson annotations (e.g. @JsonTypeInfo
, @JsonSubTypes
, @JsonProperty
) we propose to create a separate configuration class. Every devonfw application (sample or any app created from our app-template) therefore has a class called ApplicationObjectMapperFactory
that extends ObjectMapperFactory from the devon4j-rest module. It looks like this:
@Named("ApplicationObjectMapperFactory")
public class ApplicationObjectMapperFactory extends ObjectMapperFactory {
public RestaurantObjectMapperFactory() {
super();
// JSON configuration code goes here
}
}
If you are using inheritance for your objects mapped to JSON then polymorphism can not be supported out-of-the box. So in general avoid polymorphic objects in JSON mapping. However, this is not always possible. Have a look at the following example from our sample application:
Now assume you have a REST service operation as Java method that takes a ProductEto as argument. As this is an abstract class the server needs to know the actual sub-class to instantiate. We typically do not want to specify the classname in the JSON as this should be an implementation detail and not part of the public JSON format (e.g. in case of a service interface). Therefore we use a symbolic name for each polymorphic subtype that is provided as virtual attribute @type within the JSON data of the object:
{ "@type": "Drink", ... }
Therefore you add configuration code to the constructor of ApplicationObjectMapperFactory. Here you can see an example from the sample application:
setBaseClasses(ProductEto.class);
addSubtypes(new NamedType(MealEto.class, "Meal"), new NamedType(DrinkEto.class, "Drink"),
new NamedType(SideDishEto.class, "SideDish"));
We use setBaseClasses
to register all top-level classes of polymorphic objects. Further we declare all concrete polymorphic sub-classes together with their symbolic name for the JSON format via addSubtypes
.
In order to map custom datatypes or other types that do not follow the Java bean conventions, you need to define a custom mapping. If you create objects dedicated for the JSON mapping you can easily avoid such situations. When this is not suitable follow these instructions to define the mapping:
-
As an example, the use of JSR354 (
javax.money
) is appreciated in order to process monetary amounts properly. However, without custom mapping, the default mapping of Jackson will produce the following JSON for aMonetaryAmount
:"currency": {"defaultFractionDigits":2, "numericCode":978, "currencyCode":"EUR"}, "monetaryContext": {...}, "number":6.99, "factory": {...}
As clearly can be seen, the JSON contains too much information and reveals implementation secrets that do not belong here. Instead the JSON output expected and desired would be:
"currency":"EUR","amount":"6.99"
Even worse, when we send the JSON data to the server, Jackson will see that
MonetaryAmount
is an interface and does not know how to instantiate it so the request will fail. Therefore we need a customized Serializer. -
We implement
MonetaryAmountJsonSerializer
to define how aMonetaryAmount
is serialized to JSON:public final class MonetaryAmountJsonSerializer extends JsonSerializer<MonetaryAmount> { public static final String NUMBER = "amount"; public static final String CURRENCY = "currency"; public void serialize(MonetaryAmount value, JsonGenerator jgen, SerializerProvider provider) throws ... { if (value != null) { jgen.writeStartObject(); jgen.writeFieldName(MonetaryAmountJsonSerializer.CURRENCY); jgen.writeString(value.getCurrency().getCurrencyCode()); jgen.writeFieldName(MonetaryAmountJsonSerializer.NUMBER); jgen.writeString(value.getNumber().toString()); jgen.writeEndObject(); } }
For composite datatypes it is important to wrap the info as an object (
writeStartObject()
andwriteEndObject()
).MonetaryAmount
provides the information we need by thegetCurrency()
andgetNumber()
. So that we can easily write them into the JSON data. -
Next, we implement
MonetaryAmountJsonDeserializer
to define how aMonetaryAmount
is deserialized back as Java object from JSON:public final class MonetaryAmountJsonDeserializer extends AbstractJsonDeserializer<MonetaryAmount> { protected MonetaryAmount deserializeNode(JsonNode node) { BigDecimal number = getRequiredValue(node, MonetaryAmountJsonSerializer.NUMBER, BigDecimal.class); String currencyCode = getRequiredValue(node, MonetaryAmountJsonSerializer.CURRENCY, String.class); MonetaryAmount monetaryAmount = MonetaryAmounts.getAmountFactory().setNumber(number).setCurrency(currencyCode).create(); return monetaryAmount; } }
For composite datatypes we extend from
AbstractJsonDeserializer
as this makes our task easier. So we already get aJsonNode
with the parsed payload of our datatype. Based on this API it is easy to retrieve individual fields from the payload without taking care of their order, etc.AbstractJsonDeserializer
also provides methods such asgetRequiredValue
to read required fields and get them converted to the desired basis datatype. So we can easily read the amount and currency and construct an instance ofMonetaryAmount
via the official factory API. -
Finally we need to register our custom (de)serializers with the following configuration code in the constructor of ApplicationObjectMapperFactory:+
SimpleModule module = getExtensionModule(); module.addDeserializer(MonetaryAmount.class, new MonetaryAmountJsonDeserializer()); module.addSerializer(MonetaryAmount.class, new MonetaryAmountJsonSerializer());
Now we can read and write MonetaryAmount
from and to JSON as expected.