Mule 4 is modifying Java object variables - java

I'm migrating my Mule apps from 3.9.0 to Mule 4.4.0. I'm using a Java object to propagate metadata and other objects across transports. Here is a snippet of that class:
package com.test;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
public class DataWrapper implements Serializable {
private static final long serialVersionUID = 1L;
private final Map<String, Object> properties = new HashMap<>();
private Object payload;
private final Object originalPayload;
public DataWrapper(final Object payload) {
this(payload, false);
}
public DataWrapper(final Object payload, boolean playback) {
this.originalPayload = payload;
this.payload = payload;
}
#SuppressWarnings("unchecked")
public <T> T getProperty(String name, T defaultValue) {
if(properties.containsKey(name)) {
return (T) properties.get(name);
} else {
return defaultValue;
}
}
public Object getProperty(String name) {
return getProperty(name, null);
}
public void setProperty(String name, Object value) {
properties.put(name, value);
}
public void setProperties(Map<String, Object> additional) {
properties.putAll(additional);
}
public Object getPayload() {
return payload;
}
public void setPayload(Object payload) {
this.payload = payload;
}
public Object getOriginalPayload() {
return originalPayload;
}
public static DataWrapper wrapData(Object payload) {
return new DataWrapper(payload);
}
public static DataWrapper wrapData(Object payload, Map<String, Object> properties) {
DataWrapper wrapper = new DataWrapper(payload);
wrapper.setProperties(properties);
return wrapper;
}
}
the static methods being called (simplified for example):
public class MyTransformers {
public static DataWrapper addProperties(DataWrapper wrapper, Map<String, Object> properties) {
//process payload and add properties/objects
wrapper.setProperties(properties);
return wrapper;
}
public static String doSomethingWithPayload(DataWrapper wrapper, Map<String, Object> properties) {
//process payload and add properties/objects
return (String) wrapper.getPayload();
}
public static String doSomethingElse(DataWrapper wrapper) {
//do something else with payload
return (String) wrapper.getPayload();
}
}
and the flows:
<?xml version="1.0" encoding="UTF-8"?>
<mule
xmlns:sockets="http://www.mulesoft.org/schema/mule/sockets"
xmlns:vm="http://www.mulesoft.org/schema/mule/vm"
xmlns:java="http://www.mulesoft.org/schema/mule/java"
xmlns="http://www.mulesoft.org/schema/mule/core"
xmlns:doc="http://www.mulesoft.org/schema/mule/documentation"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.mulesoft.org/schema/mule/core http://www.mulesoft.org/schema/mule/core/current/mule.xsd
http://www.mulesoft.org/schema/mule/vm http://www.mulesoft.org/schema/mule/vm/current/mule-vm.xsd
http://www.mulesoft.org/schema/mule/java http://www.mulesoft.org/schema/mule/java/current/mule-java.xsd
http://www.mulesoft.org/schema/mule/sockets http://www.mulesoft.org/schema/mule/sockets/current/mule-sockets.xsd">
<vm:config name="DefaultVmConnector" doc:name="VM Config" doc:id="786119a5-8fd8-40b8-a145-bfbf5df6a505" >
<vm:queues >
<vm:queue queueName="ReportsEndpoint" />
<vm:queue queueName="PublishEndpoint" />
</vm:queues>
</vm:config>
<sockets:listener-config name="UDP_Config" doc:name="Sockets Listener config" doc:id="b2ec3926-6a0b-4612-97e8-fe9f4011fbad" >
<sockets:udp-listener-connection host="192.168.1.203" port="12121" />
</sockets:listener-config>
<flow name="TestPublishFlow" doc:id="05a95422-080b-491e-9a4d-efc82f10b399" >
<sockets:listener doc:name="Listener" doc:id="5be9224a-be1b-4779-a137-121376cd890d" config-ref="UDP_Config" outputMimeType="text/json"/>
<java:invoke-static doc:name="Data Wrapper" doc:id="9fa629cc-0520-4c00-beae-15f849568d3b" class="com.test.DataWrapper" method="wrapData(java.lang.Object)" outputMimeType="application/java">
<java:args ><![CDATA[#[{payload : payload}]]]></java:args>
</java:invoke-static>
<vm:publish doc:name="Publish to VM" doc:id="b6497fb1-3a57-468c-bcb6-089372407787" config-ref="DefaultVmConnector" sendCorrelationId="NEVER" queueName="PublishEndpoint"/>
</flow>
<flow name="publisher-flow" doc:id="4c765b98-68df-4af2-a517-73bddfe8cc25" >
<vm:listener doc:name="Publish Endpoint" doc:id="a8589268-f5ea-4ebe-a11f-760ff32c2e1f" config-ref="DefaultVmConnector" numberOfConsumers="2" queueName="PublishEndpoint"/>
<java:invoke-static doc:name="Some Business Logic" doc:id="ee53fe99-f459-4581-892e-00e09a8a10e2" class="com.test.MyTransformers" outputMimeType="application/java" method="addProperties(com.test.DataWrapper,java.util.Map)">
<java:args ><![CDATA[#[{ wrapper:payload, properties:{ ClientId:p('source.clientId'), Source:p('data.source'), Metadata:p('data.metadata'), MimeType:p('data.mimetype')}}]]]></java:args>
</java:invoke-static>
<choice doc:name="Choice" doc:id="7976bc82-67d4-4534-b47b-80ed7e287f07" >
<when expression="#[Java::isInstanceOf(payload.payload, 'com.test.DataWrapper')]">
<java:invoke-static doc:name="Do Something" doc:id="528cdafa-9be1-4433-aaf5-0e0352078757" outputMimeType="application/java" class="com.test.MyTransformers" method="doSomethingWithPayload(com.test.DataWrapper,java.util.Map)">
<java:args ><![CDATA[#[{ wrapper : payload, prettyprint : p('xml.prettyprint'), validate : p('xml.validate') }]]]></java:args>
</java:invoke-static>
</when>
<otherwise >
<java:invoke-static doc:name="Do Something Else" doc:id="6d63d9e7-12be-4d43-9990-02b897ec0cee" class="com.test.MyTransformers" method="doSomethingElse(com.test.DataWrapper)" outputMimeType="application/java">
<java:args ><![CDATA[#[{ wrapper : payload }]]]></java:args>
</java:invoke-static>
</otherwise>
</choice>
<logger level="INFO" doc:name="Placeholder" doc:id="800d22cd-55a5-4ee9-a280-497d2f276a63" message="#[payload]"/>
</flow>
</mule>
I need to keep the original message and the processed message to store for later use.
When payload and originalPayload are set to a String object in DataWrapper and the mule message goes through a VM transport, those objects become org.mule.runtime.core.internal.streaming.bytes.ManagedCursorStreamProvider.
Before sending to VM publisher the fields payload and originalpayload in DataWrapper are:
{ "count" : 0, "comment" : "This is a test"}
and after it is:
org.mule.runtime.core.internal.streaming.bytes.ManagedCursorStreamProvider#162b291f
I'm also getting a serialization exception when going through the VM endpoint:
Caused by: java.io.NotSerializableException: org.mule.runtime.core.internal.streaming.bytes.ManagedCursorStreamProvider
How can I prevent that from happening without changing the type of payload and originalPayload from Object to String?

Mule 4 implements streaming in most components, unlike Mule 3 which implemented it only for some. The payload returned by the HTTP Listener is a stream implemented by the ManagedCursorStreamProvider. It was not added by the VM connector, it was the original payload. Most components in Mule 4 now how to use the stream implementation transparently, so usually a user doesn't need to care if their JSON payload is in a stream or in a Java string. The exception is when you do actual Java code you need to know the actual classes.
It is not a good practice and discouraged to add Mule specific APIs and classes to your custom Java code to make them 'stream aware'. Instead an easier solution is to let DataWeave do its transformation magic and convert to a Java object automatically, in this case a String.
Example:
<sockets:listener doc:name="Listener" config-ref="UDP_Config" outputMimeType="text/json"/>
<ee:transform doc:name="Transform Message">
<ee:message >
<ee:set-payload ><![CDATA[%dw 2.0
output application/java
---
write(payload,"application/java")
]]>
</ee:set-payload>
</ee:message>
</ee:transform>
<java:invoke-static doc:name="Data Wrapper" class="com.test.DataWrapper" method="wrapData(java.lang.Object)" outputMimeType="application/java">
<java:args ><![CDATA[#[{payload : payload}]]]></java:args>
</java:invoke-static>
The write() function writes a string from the payload that you can use in your wrapper class. This works for this particular scenario. You will need to change classes if you do some other permutations of inputs and Java code.

Related

How to get parent observation on Spring Boot 3 using RestTemplate

I'm migrating some REST applications to Spring Boot 3 and I'm trying to set an extra low cardinality key/value, based on the value of an incoming request header (X-Traffic-Type).
When doing it for webflux applications, everything works fine: I provide an extension of DefaultServerRequestObservationConvention and set the value based on the incoming header:
public class MyCustomServerRequestObservationConvention extends DefaultServerRequestObservationConvention {
#Override
public KeyValues getLowCardinalityKeyValues(final ServerRequestObservationContext context) {
return super.getLowCardinalityKeyValues(context).and(KeyValue.of(SupplyHubMetricsAutoConfiguration.TRAFFIC_TYPE_TAG, getTrafficType(context.getCarrier())));
}
private String getTrafficType(final ServerHttpRequest request) {
return Optional.ofNullable(request.getHeaders().getFirst("X-Traffic-Type"))
.map(String::toLowerCase)
.orElse("");
}
}
For the client side, I can simply get the traffic type from the parent observation on an extension of DefaultClientRequestObservationConvention:
public class MyCustomClientRequestObservationConvention extends DefaultClientRequestObservationConvention {
#Override
public KeyValues getLowCardinalityKeyValues(final ClientRequestObservationContext context) {
return KeyValues.of(method(context), status(context), exception(context), outcome(context), trafficType(context), qualifier(context));
}
protected KeyValue trafficType(ClientRequestObservationContext context) {
String trafficType = Optional.ofNullable(context.getParentObservation())
.map(ObservationView::getContextView)
.map(e -> e.getLowCardinalityKeyValue(TRAFFIC_TYPE_TAG))
.map(KeyValue::getValue)
.orElse(DEFAULT_TRAFFIC_TYPE);
return KeyValue.of(TRAFFIC_TYPE_TAG, trafficType);
}
}
Now, when I do the same for the MVC applications, I can set the value on the server observation but when I implement the client part by extending DefaultClientRequestObservationConvention the observation doesn't have a parent observation (which I'm expecting to be the server one):
public class MyCustomClientRequestObservationConvention extends DefaultClientRequestObservationConvention {
#Override
public KeyValues getLowCardinalityKeyValues(final ClientRequestObservationContext context) {
return KeyValues.of(method(context), status(context), exception(context), outcome(context), trafficType(context), qualifier());
}
protected KeyValue trafficType(ClientRequestObservationContext context) {
// context.getParentObservation() always returns null
String trafficType = Optional.ofNullable(context.getParentObservation())
.map(ObservationView::getContextView)
.map(e -> e.getLowCardinalityKeyValue(TRAFFIC_TYPE_TAG))
.map(KeyValue::getValue)
.orElse(DEFAULT_TRAFFIC_TYPE);
return KeyValue.of(TRAFFIC_TYPE_TAG, trafficType);
}
Am I doing something wrong here? what's the reason why webflux observations keep the hierarchy but mvc ones seems not to?

Changing how Swagger generates the JSON response in Java

I use Swagger to set up an REST Api Service. For this I used following versions:
swagger (YAML)-file version 2.0
swagger codegen cli version v2.3.0
springfox version 2.5.0
The task of the API Service is to respond with data that we store in our database, as well as its relations.
However, we have circular relations in our data. This means that each Object a can have a relation to Object b, which can have a backward relation to Object a.
Now using Swagger, this will generate an endless long JSON, resulting in an error.
The code looks like this:
public class Service extends DataObject {
#SerializedName("provides")
private List<Utility> provides;
/**
* Get provides
*
* #return provides
**/
#ApiModelProperty(required = true, value = "")
public List<Utility> getProvides() {
return provides;
}
}
public class Utility extends DataObject {
#SerializedName("providedBy")
private List<Service> providedBy;
/**
* Get providedBy
*
* #return providedBy
**/
#ApiModelProperty(required = true, value = "")
public List<Service> getProvidedBy() {
return providedBy;
}
}
Then in the response:
#Override
public ResponseEntity<List<Service>> servicesGet(
#Min(0) #ApiParam(value = "The number of layers of relations the object is enriched by. Lower numbers typically increase performance.", defaultValue = "0") #Valid #RequestParam(value = "layersOfRelations", required = false, defaultValue = "0") final Integer layersOfRelations) {
String accept = this.request.getHeader("Accept");
List<Service> services = Util.getServices();
return new ResponseEntity<List<Service>>(services, HttpStatus.OK);
}
My Question is, where and how can I change the output that will be automatically generated for the return in the method servicesGet()?
I would like to not transform the whole object b into JSON, but rather only its title, so that there will be no endless recursion.
So after some research I found out, that swagger is using Jackson to convert objects into JSON (even though the #SerializedName parameter contained in the generated code was from the gson library, at least in my case).
This means that in this case it I can simply use customization techniques that Jackson provides, and it worked like a charm.
I only had to write a custom Jackson adapter annotation:
#SerializedName("provides")
#JsonSerialize(using = DataObjectAdapter.class)
private List<Utility> provides;
and the adapter looks like this:
public static class DataObjectAdapter extends JsonSerializer<List<Utility>> {
#Override
public void serialize(final List<Utility> arg0, final JsonGenerator arg1, final SerializerProvider arg2)
throws IOException {
arg1.writeStartArray();
for (Utility object : arg0) {
arg1.writeStartObject();
arg1.writeStringField("id", object.getId());
arg1.writeStringField("title", object.getTitle());
arg1.writeStringField("description", object.getDescription());
arg1.writeEndObject();
}
arg1.writeEndArray();
}
}
So now instead of writing the whole object into JSON (and thus recursivly adding all child objects into it, he will only write the information I described in the adapter into JSON.
Similarly a deserializer, where I read out the ID in the JSON and map it to my data, would look like this
public static class ServiceDeserializer extends StdDeserializer<Service> {
public ServiceDeserializer() {
super(Service.class);
}
#Override
public Service deserialize(final JsonParser jp, final DeserializationContext ctxt)
throws IOException, JsonProcessingException {
List<Utility> objects = Util.getUtilities();
JsonNode node = jp.getCodec().readTree(jp);
String id = node.get("id").asText();
String title = node.get("title").asText();
String description = node.get("description").asText();
Service service = new Service(id, title, description);
for (JsonNode utility : node.get("provides")) {
String checkId = utility.get("id").asText();
for (DataObject object : objects) {
if (object.getId().equals(checkId)) {
service.addUtility(object);
break;
}
}
}
return service;
}
}

How to conditionally deserialize to a POJO field using Jackson?

How can I conditionally deserialize a JSON string to a POJO field?
I receive a JSON string like so:
{
"status": "we stuck",
"data" : "someData"
}
but "someData" can be just a string "under the bridge" or can be something like "['bridge 5', 'Mandela bridge']" or "[{'incident 1' : '['bridge 1', 'bridge 2]'},{'incident 2' : ['bridge 99', 'what ever else']}]"
I want to return the json string AS IS if "data" is an array then I will map to a different Type that deals with the array
I have a java class:
class Response {
String status;
String data;
}
the other Type will have data as
ArrayList<SomeOtherType> data;
This is what i have so far
ObjectMapper mapper = new ObjectMapper();
Response rspns = mapper.readValue(<theJSONStrHere>, Response.class);
this fails when data is an array, giving me the message
can not deserialize instance of java.lang.String out of START_ARRAY token
I don't know where to go from here.
You can either use a custom deserializer as aussie said or you can just modify your working solution.
class Response {
String status;
String data;
}
class Other {
String status;
ArrayList<SomeOtherType> data;
}
ObjectMapper mapper = new ObjectMapper();
Other rspns = mapper.readValue(<theJSONStrHere>, Other.class);
This will parse the JSON String to the Other class with the ArrayList.
Now it's your turn to implement the decision of then to use
Other rspns = mapper.readValue(<theJSONStrHere>, Other.class);
or when to use
Responserspns = mapper.readValue(<theJSONStrHere>, Response.class);
Note: The above is a quick and dirty solution. It works like that but I would highly recommend to use a custom deserializer, which handles the logic of what it is and what to return.
Also keep in mind that for this to work the best you might consider building the POJO structure to multiple classes which extend a base class and then work generic.
example:
class response {
String status;
}
class simpleResponse extends response {
String data;
}
class listResponse extends response {
ArrayList<Type> data;
}
class MyDeserializer extends JSONDeserializer<E extends response> {
public E deserialize...) {
}
}
To get an actual working example read about Jackson
Dont make it complicated think simple..
There are two ways
First taking List os string/(or any other type)
private List<String> tags;
Second taking List of class (if you need more than one parameters)
List<PageLink> pagelinks;
See below case example......
public class PagesJson {
private String ln;
private int pageno;
private List<String> tags;
private List<PageLink> pagelinks;
private String error;
}
public class PageLink {
private String title= null;
private String url;
}
Now json of PagesJson class as below
{"ln":en,"count":100,"viewcount":23,"pageno":17,"tags":["Ensuring safe motherhood","pregnancy health in women","Abortion"],"pagelinks":[{"title":"Abortion","url":"http://vikaspedia.in/health/women-health"},{"title":"Acts and Rules","url":"http://vikaspedia.in/social-welfare/scheduled-tribes-welfare/acts-and-rules"},{"title":"Acts and Rules ","url":"http://vikaspedia.in/social-welfare/unorganised-sector-1/acts-and-rules"}],"error":"Parameter Validation Error"}
{"ln":en,"count":100,"viewcount":23,"pageno":17,"tags":["Ensuring safe motherhood","pregnancy health in women","Abortion"],"pagelinks":[{"title":"Abortion","url":"http://vikaspedia.in/health/women-health"},{"title":"Acts and Rules","url":"http://vikaspedia.in/social-welfare/scheduled-tribes-welfare/acts-and-rules"},{"title":"Acts and Rules ","url":"http://vikaspedia.in/social-welfare/unorganised-sector-1/acts-and-rules"}],"error":"Parameter Validation Error"}
For Mapping json to class use jackson library as below.....
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
....
.....
ObjectMapper mapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
PagesJson pj = mapper.readValue(response.toString(), PagesJson.class);
You can use Custom Deserializer on a Method OR a Class using below :
extends JsonDeserializer
then
#Override
public ReturnObject deserialize(JsonParser parser, DeserializationContext ctx)
throws IOException, JsonProcessingException {
JsonToken token = parser.getCurrentToken();
if(JsonToken.START_ARRAY.equals(token)){
//TODO your JSON Array code handling
}else{
//TO DO you JSON Object Handling
}
}

Spring Boot: Wrapping JSON response in dynamic parent objects

I have a REST API specification that talks with back-end microservices, which return the following values:
On "collections" responses (e.g. GET /users) :
{
users: [
{
... // single user object data
}
],
links: [
{
... // single HATEOAS link object
}
]
}
On "single object" responses (e.g. GET /users/{userUuid}) :
{
user: {
... // {userUuid} user object}
}
}
This approach was chosen so that single responses would be extensible (for example, maybe if GET /users/{userUuid} gets an additional query parameter down the line such at ?detailedView=true we would have additional request information).
Fundamentally, I think it is an OK approach for minimizing breaking changes between API updates. However, translating this model to code is proving very arduous.
Let's say that for single responses, I have the following API model object for a single user:
public class SingleUserResource {
private MicroserviceUserModel user;
public SingleUserResource(MicroserviceUserModel user) {
this.user = user;
}
public String getName() {
return user.getName();
}
// other getters for fields we wish to expose
}
The advantage of this method is that we can expose only the fields from the internally used models for which we have public getters, but not others. Then, for collections responses I would have the following wrapper class:
public class UsersResource extends ResourceSupport {
#JsonProperty("users")
public final List<SingleUserResource> users;
public UsersResource(List<MicroserviceUserModel> users) {
// add each user as a SingleUserResource
}
}
For single object responses, we would have the following:
public class UserResource {
#JsonProperty("user")
public final SingleUserResource user;
public UserResource(SingleUserResource user) {
this.user = user;
}
}
This yields JSON responses which are formatted as per the API specification at the top of this post. The upside of this approach is that we only expose those fields that we want to expose. The heavy downside is that I have a ton of wrapper classes flying around that perform no discernible logical task aside from being read by Jackson to yield a correctly formatted response.
My questions are the following:
How can I possibly generalize this approach? Ideally, I would like to have a single BaseSingularResponse class (and maybe a BaseCollectionsResponse extends ResourceSupport class) that all my models can extend, but seeing how Jackson seems to derive the JSON keys from the object definitions, I would have to user something like Javaassist to add fields to the base response classes at Runtime - a dirty hack that I would like to stay as far away from as humanly possible.
Is there an easier way to accomplish this? Unfortunately, I may have a variable number of top-level JSON objects in the response a year from now, so I cannot use something like Jackson's SerializationConfig.Feature.WRAP_ROOT_VALUE because that wraps everything into a single root-level object (as far as I am aware).
Is there perhaps something like #JsonProperty for class-level (as opposed to just method and field level)?
There are several possibilities.
You can use a java.util.Map:
List<UserResource> userResources = new ArrayList<>();
userResources.add(new UserResource("John"));
userResources.add(new UserResource("Jane"));
userResources.add(new UserResource("Martin"));
Map<String, List<UserResource>> usersMap = new HashMap<String, List<UserResource>>();
usersMap.put("users", userResources);
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.writeValueAsString(usersMap));
You can use ObjectWriter to wrap the response that you can use like below:
ObjectMapper mapper = new ObjectMapper();
ObjectWriter writer = mapper.writer().withRootName(root);
result = writer.writeValueAsString(object);
Here is a proposition for generalizing this serialization.
A class to handle simple object:
public abstract class BaseSingularResponse {
private String root;
protected BaseSingularResponse(String rootName) {
this.root = rootName;
}
public String serialize() {
ObjectMapper mapper = new ObjectMapper();
ObjectWriter writer = mapper.writer().withRootName(root);
String result = null;
try {
result = writer.writeValueAsString(this);
} catch (JsonProcessingException e) {
result = e.getMessage();
}
return result;
}
}
A class to handle collection:
public abstract class BaseCollectionsResponse<T extends Collection<?>> {
private String root;
private T collection;
protected BaseCollectionsResponse(String rootName, T aCollection) {
this.root = rootName;
this.collection = aCollection;
}
public T getCollection() {
return collection;
}
public String serialize() {
ObjectMapper mapper = new ObjectMapper();
ObjectWriter writer = mapper.writer().withRootName(root);
String result = null;
try {
result = writer.writeValueAsString(collection);
} catch (JsonProcessingException e) {
result = e.getMessage();
}
return result;
}
}
And a sample application:
public class Main {
private static class UsersResource extends BaseCollectionsResponse<ArrayList<UserResource>> {
public UsersResource() {
super("users", new ArrayList<UserResource>());
}
}
private static class UserResource extends BaseSingularResponse {
private String name;
private String id = UUID.randomUUID().toString();
public UserResource(String userName) {
super("user");
this.name = userName;
}
public String getUserName() {
return this.name;
}
public String getUserId() {
return this.id;
}
}
public static void main(String[] args) throws JsonProcessingException {
UsersResource userCollection = new UsersResource();
UserResource user1 = new UserResource("John");
UserResource user2 = new UserResource("Jane");
UserResource user3 = new UserResource("Martin");
System.out.println(user1.serialize());
userCollection.getCollection().add(user1);
userCollection.getCollection().add(user2);
userCollection.getCollection().add(user3);
System.out.println(userCollection.serialize());
}
}
You can also use the Jackson annotation #JsonTypeInfo in a class level
#JsonTypeInfo(include=As.WRAPPER_OBJECT, use=JsonTypeInfo.Id.NAME)
Personally I don't mind the additional Dto classes, you only need to create them once, and there is little to no maintenance cost. And If you need to do MockMVC tests, you will most likely need the classes to deserialize your JSON responses to verify the results.
As you probably know the Spring framework handles the serialization/deserialization of objects in the HttpMessageConverter Layer, so that is the correct place to change how objects are serialized.
If you don't need to deserialize the responses, it is possible to create a generic wrapper, and a custom HttpMessageConverter (and place it before MappingJackson2HttpMessageConverter in the message converter list). Like this:
public class JSONWrapper {
public final String name;
public final Object object;
public JSONWrapper(String name, Object object) {
this.name = name;
this.object = object;
}
}
public class JSONWrapperHttpMessageConverter extends MappingJackson2HttpMessageConverter {
#Override
protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
// cast is safe because this is only called when supports return true.
JSONWrapper wrapper = (JSONWrapper) object;
Map<String, Object> map = new HashMap<>();
map.put(wrapper.name, wrapper.object);
super.writeInternal(map, type, outputMessage);
}
#Override
protected boolean supports(Class<?> clazz) {
return clazz.equals(JSONWrapper.class);
}
}
You then need to register the custom HttpMessageConverter in the spring configuration which extends WebMvcConfigurerAdapter by overriding configureMessageConverters(). Be aware that doing this disables the default auto detection of converters, so you will probably have to add the default yourself (check the Spring source code for WebMvcConfigurationSupport#addDefaultHttpMessageConverters() to see defaults. if you extend WebMvcConfigurationSupport instead WebMvcConfigurerAdapter you can call addDefaultHttpMessageConverters directly (Personally I prefere using WebMvcConfigurationSupport over WebMvcConfigurerAdapter if I need to customize anything, but there are some minor implications to doing this, which you can probably read about in other articles.
Jackson doesn't have a lot of support for dynamic/variable JSON structures, so any solution that accomplishes something like this is going to be pretty hacky as you mentioned. As far as I know and from what I've seen, the standard and most common method is using wrapper classes like you are currently. The wrapper classes do add up, but if you get creative with your inheretence you may be able to find some commonalities between classes and thus reduce the amount of wrapper classes. Otherwise you might be looking at writing a custom framework.
I guess you are looking for Custom Jackson Serializer. With simple code implementation same object can be serialized in different structures
some example:
https://stackoverflow.com/a/10835504/814304
http://www.davismol.net/2015/05/18/jackson-create-and-register-a-custom-json-serializer-with-stdserializer-and-simplemodule-classes/

Jackson JSON, filtering properties by path

I need to filter bean properties dynamiclly on serialization.
The #JsonView isn't an option for me.
Assume my Bean (as Json notation):
{
id: '1',
name: 'test',
children: [
{ id: '1.1', childName: 'Name 1.1' },
{ id: '1.2', childName: 'Name 1.2' }
]
}
I want to write the JSON with the following properties:
// configure the ObjectMapper to only serialize this properties:
[ "name", "children.childName" ]
The expected JSON result is:
{
name: 'test',
children: [
{ childName: 'Name 1.1' },
{ childName: 'Name 1.2' }
]
}
Finally I will create an annotation (#JsonFilterProperties) to use with Spring in my RestControllers, something like this:
#JsonFilterProperties({"name", "children.childName"}) // display only this fields
#RequestMapping("/rest/entity")
#ResponseBody
public List<Entity> findAll() {
return serviceEntity.findAll(); // this will return all fields populated!
}
Well, it's tricky but doable. You can do this using Jacksons Filter feature (http://wiki.fasterxml.com/JacksonFeatureJsonFilter) with some minor alterations. To start, we are going to use class name for filter id, this way you won't have to add #JsonFIlter to every entity you use:
public class CustomIntrospector extends JacksonAnnotationIntrospector {
#Override
public Object findFilterId(AnnotatedClass ac) {
return ac.getRawType();
}
}
Next step, make that filter of super class will apply to all of its subclasses:
public class CustomFilterProvider extends SimpleFilterProvider {
#Override
public BeanPropertyFilter findFilter(Object filterId) {
Class id = (Class) filterId;
BeanPropertyFilter f = null;
while (id != Object.class && f == null) {
f = _filtersById.get(id.getName());
id = id.getSuperclass();
}
// Part from superclass
if (f == null) {
f = _defaultFilter;
if (f == null && _cfgFailOnUnknownId) {
throw new IllegalArgumentException("No filter configured with id '" + filterId + "' (type " + filterId.getClass().getName() + ")");
}
}
return f;
}
}
Custom version of ObjectMapper that utilizes our custom classes:
public class JsonObjectMapper extends ObjectMapper {
CustomFilterProvider filters;
public JsonObjectMapper() {
filters = new CustomFilterProvider();
filters.setFailOnUnknownId(false);
this.setFilters(this.filters);
this.setAnnotationIntrospector(new CustomIntrospector());
}
/* You can change methods below as you see fit. */
public JsonObjectMapper addFilterAllExceptFilter(Class clazz, String... property) {
filters.addFilter(clazz.getName(), SimpleBeanPropertyFilter.filterOutAllExcept(property));
return this;
}
public JsonObjectMapper addSerializeAllExceptFilter(Class clazz, String... property) {
filters.addFilter(clazz.getName(), SimpleBeanPropertyFilter.serializeAllExcept(property));
return this;
}
}
Now take a look at MappingJackson2HttpMessageConverter, you will see that it uses one instane of ObjectMapper internaly, ergo you cannot use it if you want different configurations simultaneously (for different requests). You need request scoped ObjectMapper and appropriate message converter that uses it:
public abstract class DynamicMappingJacksonHttpMessageConverter extends MappingJackson2HttpMessageConverter {
// Spring will override this method with one that provides request scoped bean
#Override
public abstract ObjectMapper getObjectMapper();
#Override
public void setObjectMapper(ObjectMapper objectMapper) {
// We dont need that anymore
}
/* Additionally, you need to override all methods that use objectMapper attribute and change them to use getObjectMapper() method instead */
}
Add some bean definitions:
<bean id="jsonObjectMapper" class="your.package.name.JsonObjectMapper" scope="request">
<aop:scoped-proxy/>
</bean>
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="your.package.name.DynamicMappingJacksonHttpMessageConverter">
<lookup-method name="getObjectMapper" bean="jsonObjectMapper"/>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
And the last part is to implement something that will detect your annotation and perform actual configuration. For that you can create an #Aspect. Something like:
#Aspect
public class JsonResponseConfigurationAspect {
#Autowired
private JsonObjectMapper objectMapper;
#Around("#annotation(jsonFilterProperties)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
/* Here you will have to determine return type and annotation value from jointPoint object. */
/* See http://stackoverflow.com/questions/2559255/spring-aop-how-to-get-the-annotations-of-the-adviced-method for more info */
/* If you want to use things like 'children.childName' you will have to use reflection to determine 'children' type, and so on. */
}
}
Personally, I use this in a different way. I dont use annotations and just do configuration manually:
#Autowired
private JsonObjectMapper objectMapper;
#RequestMapping("/rest/entity")
#ResponseBody
public List<Entity> findAll() {
objectMapper.addFilterAllExceptFilter(Entity.class, "name", "children");
objectMapper.addFilterAllExceptFilter(EntityChildren.class, "childName");
return serviceEntity.findAll();
}
P.S. This approach has one major flaw: you cannot add two different filters for one class.
There's Jackson plugin called squiggly for doing exactly this.
String filter = "name,children[childName]";
ObjectMapper mapper = Squiggly.init(this.objectMapper, filter);
mapper.writeValue(response.getOutputStream(), myBean);
You could integrate it into a MessageConverter or similar, driven by annotations, as you see fit.
If you have a fixed number of possible options, then there is a static solution too: #JsonView
public interface NameAndChildName {}
#JsonView(NameAndChildName.class)
#ResponseBody
public List<Entity> findAll() {
return serviceEntity.findAll();
}
public class Entity {
public String id;
#JsonView(NameAndChildName.class)
public String name;
#JsonView({NameAndChildName.class, SomeOtherView.class})
public List<Child> children;
}
public class Child {
#JsonView(SomeOtherView.class)
public String id;
#JsonView(NameAndChildName.class)
public String childName;
}

Categories