I have an object that is used as a return type in a REST web service (jax-rs). The object contains a field which is an array of another type. Example:
#Name("PARAMETER_DEFINITION_TABTYPE")
#TableOfDefinition
#XmlType(
name = "parameter_DEFINITION_TABTYPE"
)
class PARAMETER_DEFINITION_TABTYPE {
#XmlElement(
name = "parameter_definition_rectype",
nillable = true
)
public PARAMETER_DEFINITION_RECTYPE[] ELEMENTS;
#Override
public String toString() {
return ELEMENTS == null ? null : java.util.Arrays.toString(ELEMENTS);
}
}
I use all the existing annotations to create my SOAP web services and don't want to touch the class or the existing annotations. The REST service I create uses the same class and generates the following json:
{"parameter_definition_rectype": [
{
"name": "abc"
},
{
"name": "abss"
}
]}
I would like to get the following output (basically ignore the outer element and use only the "ELEMENTS" field):
[
{
"name": "abc"
},
{
"name": "abss"
}
]
I also want to ignore the outer object when the PARAMETER_DEFINITION_TABTYPE is nested in another Object.
Is there a way I can achieve this by using annotations?
Thanks!
You could define a custom serializer and deserializer for PARAMETER_DEFINITION_TABTYPE class which will change the way it's processed by ObjectMapper.
This is explained in the wiki Jackson How-To: Custom Serializers, if you can't add new annotations to PARAMETER_DEFINITION_TABTYPE class you should use a custom module.
It's more or less the below code. The problem here is that you have to provide a lot of custom code to get desired behavior. It would be cleaner to have a separate class to represent the JSON object because it's different than XML object:
public static void main(String[] args) throws Exception {
SimpleModule module = new SimpleModule("WrapperModule", new Version(1, 0, 0, null));
module.addSerializer(Wrapper.class, new WrapperSerializer(Wrapper.class));
module.addDeserializer(Wrapper.class, new WrapperDeserializer(Wrapper.class));
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);
Wrapper in = new Wrapper();
in.elements = new String[]{"a", "b", "c"};
String json = mapper.writeValueAsString(in);
System.out.println(json);
Wrapper out = mapper.readValue(json, Wrapper.class);
System.out.println(Arrays.toString(out.elements));
}
public static class Wrapper {
public String[] elements;
}
public static class WrapperSerializer extends StdSerializer<Wrapper> {
public WrapperSerializer(Class<Wrapper> t) {
super(t);
}
#Override
public void serialize(Wrapper w, JsonGenerator gen, SerializerProvider provider) throws IOException {
provider.defaultSerializeValue(w.elements, gen);
}
}
public static class WrapperDeserializer extends StdDeserializer<Wrapper> {
public WrapperDeserializer(Class<Wrapper> c) {
super(c);
}
#Override
public Wrapper deserialize(JsonParser p, DeserializationContext ctx) throws IOException {
Wrapper w = new Wrapper();
w.elements = ctx.readValue(p, String[].class);
return w;
}
}
I have a problem with wrong objects in lists. For instance I've a JSON model:
{
"items": [
{
"id": 1,
"name": "Item1"
},
{
"id": 2,
"name": "Item2"
},
{
"id": [],
"name": "Item3"
}
]
}
and two POJO
data class BadList(val items: List<BadItem>)
data class BadItem(val id: Int, val name: String)
Of course, when the parser stumbles upon a third element I get the exception
com.fasterxml.jackson.databind.JsonMappingException: Can not deserialize instance of java.lang.Integer out of START_ARRAY token
at [Source: {"items":[{"id":1,"name":"Item1"},{"id":2,"name":"Item2"},{"id":[],"name":"Item3"}]}; line: 1, column: 19] (through reference chain: my.package.BadList["items"]->java.util.ArrayList[2]->my.package.BadItem["id"])
Who knows how to get around this? I want to skip that wrong item.
You can write a custom deserializer and implement deserialization logic in it, e.g.:
class ItemIdDeserialiser extends JsonDeserializer<Integer> {
#Override
public Integer deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
Object value = p.getCurrentValue();
//Check if it's Integer
if(value instanceof Integer){
return (Integer) value;
}
return null; //Or return first element if it's a non empty list
}
}
Once this is done, you can annotate the field with #JsonDeserialise to instruct jackson to use your class, e.g.:
class Item {
#JsonDeserialize(using = ItemIdDeserialiser.class)
private Integer id;
}
Update
If you just want to ignore the field in serialization/deserialization then you can annotate it with #JsonIgnore, e.g.
class Item {
#JsonIgnore
private Integer id;
}
Or even better, remove id from pojo and add #JsonIgnoreProperties on the class, e.g.:
#JsonIgnoreProperties(ignoreUnknown = true)
class Item {
}
It will automatically ignore the properties which are present in json but not found in class.
You can use a "HidableSerializer" for this and check the data during serialization
1. Create a IHidable interface
The interface has a isHidden method which is called during serialization
package ch.hasselba.jackson.test;
public interface IHidable {
public boolean isHidden();
}
2. Change your BadItem class
Add the interface and change the setter of id. When property id is deserialized, it is tested if it is an Integer. If not, the item is marked as bad.
package ch.hasselba.jackson.test;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
#JsonIgnoreProperties( {"hidden"} )
public class BadItem implements IHidable{
private Integer id;
public String name;
private boolean isBadItem;
public Integer getId(){
return id;
}
public void setId(Object value){
if( value instanceof Integer ){
this.id = (Integer) value;
}else{
this.isBadItem = true;
}
}
public boolean isHidden() {
return isBadItem;
}
}
3. Create a HidableSerializer
package ch.hasselba.jackson.test;
import java.io.IOException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
#SuppressWarnings("serial")
public class HidableSerializer<T> extends StdSerializer<T> {
private JsonSerializer<T> defaultSerializer;
protected HidableSerializer(Class<T> t) {
super(t);
}
public JsonSerializer<T> getDefaultSerializer() {
return defaultSerializer;
}
public void setDefaultSerializer(JsonSerializer<T> defaultSerializer) {
this.defaultSerializer = defaultSerializer;
}
#Override
public void serialize(T value, JsonGenerator jgen, SerializerProvider provider)
throws IOException, JsonProcessingException {
if( value instanceof IHidable ){
IHidable hidableValue = (IHidable) value;
if( hidableValue.isHidden() )
return;
}
defaultSerializer.serialize(value, jgen, provider);
}
}
4. Register the HidableSerializer and that's it
package ch.hasselba.jackson.test;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
public class Demo {
#SuppressWarnings("serial")
public static void main(String[] args) {
// register the HidableSerializer
ObjectMapper mapper = new ObjectMapper();
mapper.setSerializationInclusion(Include.NON_EMPTY);
mapper.registerModule(new SimpleModule() {
#Override
public void setupModule(SetupContext context) {
super.setupModule(context);
context.addBeanSerializerModifier(new BeanSerializerModifier() {
#Override
public JsonSerializer<?> modifySerializer(
SerializationConfig config, BeanDescription desc, JsonSerializer<?> serializer) {
if (BadItem.class.isAssignableFrom(desc.getBeanClass())) {
HidableSerializer ser = new HidableSerializer(BadItem.class);
ser.setDefaultSerializer(serializer);
return ser;
}
return serializer;
}
});
}
});
String content = "{ \"items\": [ { \"id\": 1, \"name\": \"Item1\" }, { \"id\": 2, \"name\": \"Item2\" }, { \"id\":[], \"name\": \"Item3\" } ]}";
// build the Object
BadList test = null;
try {
test = mapper.readValue(content, BadList.class);
} catch (Exception e) {
e.printStackTrace();
}
// and now convert it back to a String
String data = null;
try {
data = mapper.writeValueAsString(test);
} catch (Exception e) {
e.printStackTrace();
}
// print the result
System.out.println( data );
}
}
When changing the id "[]" to an Integer value, the Item is displayed, otherwise it is empty.
The result:
{"items":[{"id":1,"name":"Item1"},{"id":2,"name":"Item2"}]}
I am currently working on an app in which I need to serialize a
HashMap<Object1, Object2> into JSON and then deserialize from JSON to the same `HashMap'.
I am able to serialize it using the usual mapper and overriding the toString() method for Object1.
public String toString(){
String res = Object1.elem1 + ";" + Object1.elem2;
return res
}
I am then able to serialize and get the expected json (where res is the String I defined before easier not to write it all back).*
{res : Object2JsonRepresentation}
Then I want to deserialize, so I use a custom keyDeserializer :
#XmlElement(name="myMap")
#JsonDeserialize(keyUsing = Object1KeyDeserializer.class)
public HashMap <Object1,Object2> myMap = new HashMap <>();
And the Object1KeyDeserializer:
public class Object1KeyDeserializer extends KeyDeserializer{
#Override
public Object1 deserializeKey(String key, DeserializationContext ctxt) throws IOException, JsonProcessingException {
String[] parts = key.split(";");
System.out.println(key);
Elem elem1 = new Elem(parts[1]);
Elem elem2 = new Elem(parts[2]);
Object1 obj = new Object1(elem1,elem2);
return obj;
}
}
Nonetheless, the keyDeserializer never seems to be called, can you explain me the reason. I'm quite new to JSON and would be glad if answers could be detailed.
Instead of using toString() you can create your own serialization format. If you have non primitive key in Map then you can serialize Map as
[
{
"key": <serialized key>,
"value: <serialized value>
},
....
]
In this case your Serializer and Deserializer will be following:
public class CustomSerializer extends StdSerializer<Map<Object1, Object2>> {
protected CustomSerializer() {
super(Map.class, true);
}
#Override
public void serialize(Map<Object1, Object2> map,
JsonGenerator jsonGenerator,
SerializerProvider serializerProvider) throws IOException{
jsonGenerator.writeStartArray();
for (Map.Entry<Object1,Object2> element: map.entrySet()) {
jsonGenerator.writeStartObject();
jsonGenerator.writeObjectField("key", element.getKey());
jsonGenerator.writeObjectField("value", element.getValue());
jsonGenerator.writeEndObject();
}
jsonGenerator.writeEndArray();
}
}
and
public class CustomDeserializer extends StdDeserializer<Map<Object1, Object2>> {
protected CustomDeserializer() {
super(Map.class);
}
#Override
public Map<Object1, Object2> deserialize(JsonParser jsonParser,
DeserializationContext deserializationContext) throws IOException {
Map<Object1, Object2> result = new HashMap<>();
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
for (JsonNode element : node) {
result.put(
jsonParser.getCodec().treeToValue(element.get("key"), Object1.class),
jsonParser.getCodec().treeToValue(element.get("value"), Object2.class)
);
}
return result;
}
}
So you can create class with your field and another Map (for checking that maps with different types works as usual):
public class MapWrapper {
#JsonSerialize(using = CustomSerializer.class)
#JsonDeserialize(using = CustomDeserializer.class)
private Map<Object1, Object2> map = new HashMap<>();
private Map<String, String> someMap = new HashMap<>();
// default constructor, getters, setters
}
Serialized value can be following:
{
"map": [
{
"key": {
"elem1": "qqq",
"elem2": "rrr"
},
"value": {
"fieldFromValue": "xxx"
}
},
{
"key": {
"elem1": "qqq_two",
"elem2": "rrr_two"
},
"value": {
"fieldFromValue": "yyy"
}
}
],
"someMap": {
"key1": "value1"
}
}
I am trying to play with the Baidu Push Notification RESTFUL API, however, I failed to figure out how to serialize and deserialize IOS Message object by Jackson with annotation.
Json Example of Target IOS Message
{
"aps": {
"alert":"Message From Baidu Cloud Push-Service",
"sound":"", //可选
"badge":0, //可选
},
"key1":"value1",
"key2":"value2"
}
"key1":"value1" and "key2":"value2" comes from a Map.
My IosApsMessage object
import com.fasterxml.jackson.annotation.JsonProperty;
public class IosApsMessage {
#JsonProperty("alert")
private String alert; //REQUIRED
#JsonProperty("sound")
private String sound;
#JsonProperty("badge")
private Integer badge;
public String getAlert() {
return alert;
}
public void setAlert(String alert) {
this.alert = alert;
}
public String getSound() {
return sound;
}
public void setSound(String sound) {
this.sound = sound;
}
public Integer getBadge() {
return badge;
}
public void setBadge(Integer badge) {
this.badge = badge;
}
}
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonProperty;
public class IosNotificationMessage {
#JsonProperty("aps")
private IosApsMessage aps;
#JsonProperty("custom_content")
private Map<String, Object> customContent;
public IosApsMessage getAps() {
return aps;
}
public void setAps(IosApsMessage aps) {
this.aps = aps;
}
public Map<String, Object> getCustomContent() {
return customContent;
}
public void setCustomContent(Map<String, Object> customContent) {
this.customContent = customContent;
}
}
My serialize result json
{
"aps": {
"alert": "alert",
"sound": "sound",
"badge": 1
},
"custom_content": {
"category": "freetrail",
"type": "state-change",
"status": "rescheduled"
}
}
What I want :
{
"aps": {
"alert": "alert",
"sound": "sound",
"badge": 1
},
"category": "freetrail",
"type": "state-change",
"status": "rescheduled"
}
I don't want the custom_content to be displayed, but I want attribute in custom_content. how can I solve this problem?
I was able to achieve the requested output through a custom serializer. The details will follow. This solution has pros and cons:
Pros
It gets the required output
It allows total freedom in how you generate the output
Cons
It seems that implementing custom serilalizer overrides all other metadata of the target class. i.e. the custom serilalizer ignores annotations of IosNotificationMessage (for example, property names). so you have to supply all the info in the code.
Here is the custom serilalizer:
public static class IosNotificationMessageSerializer extends JsonSerializer<IosNotificationMessage>
{
#Override
public void serialize(IosNotificationMessage msg, JsonGenerator gen, SerializerProvider serializers)
throws IOException, JsonProcessingException
{
gen.writeStartObject();
// serialize IosApsMessage
gen.writeObjectField("aps", msg.getAps());
// serialize map entries sequentially, thus skipping map name
for (Map.Entry<String, Object> customContentEntry : msg.getCustomContent().entrySet()) {
gen.writeObjectField(customContentEntry.getKey(), customContentEntry.getValue());
}
gen.writeEndObject();
}
}
Here is the annotation that associates the custom serilalizer to the application class:
#JsonSerialize(using = IosNotificationMessageSerializer.class)
public class IosNotificationMessage {
...
Calling new ObjectMapper().writeValue(... in the usual manner produces:
{"aps":{"alert":"Message From Baidu Cloud Push-Service","sound":"","badge":0},"key1":"value1","key2":"value2"}
I am developing a REST interface for my app using Jackson to serialize my POJO domain objects to JSON representation. I want to customize the serialization for some types to add additional properties to the JSON representation that do not exist in POJOs (e.g. add some metadata, reference data, etc). I know how to write my own JsonSerializer, but in that case I would need to explicitly call JsonGenerator.writeXXX(..) methods for each property of my object while all I need is just to add an additional property. In other words I would like to be able to write something like:
#Override
public void serialize(TaxonomyNode value, JsonGenerator jgen, SerializerProvider provider) {
jgen.writeStartObject();
jgen.writeAllFields(value); // <-- The method I'd like to have
jgen.writeObjectField("my_extra_field", "some data");
jgen.writeEndObject();
}
or (even better) to somehow intercept the serialization before the jgen.writeEndObject() call, e.g.:
#Override void beforeEndObject(....) {
jgen.writeObjectField("my_extra_field", "some data");
}
I thought I could extend BeanSerializer and override its serialize(..) method but it's declared final and also I couldn't find an easy way to create a new instance of BeanSerializer without providing it with all the type metadata details practically duplicating a good portion of Jackson. So I've given up on doing that.
My question is - how to customize Jackson's serialization to add additional stuff to the JSON output for particular POJOs without introducing too much of the boilerplate code and reusing as much as possible of the default Jackson behaviour.
Jackson 2.5 introduced the #JsonAppend annotation, which can be used to add "virtual" properties during serialization. It can be used with the mixin functionality to avoid modifying the original POJO.
The following example adds an ApprovalState property during serialization:
#JsonAppend(
attrs = {
#JsonAppend.Attr(value = "ApprovalState")
}
)
public static class ApprovalMixin {}
Register the mixin with the ObjectMapper:
mapper.addMixIn(POJO.class, ApprovalMixin.class);
Use an ObjectWriter to set the attribute during serialization:
ObjectWriter writer = mapper.writerFor(POJO.class)
.withAttribute("ApprovalState", "Pending");
Using the writer for serialization will add the ApprovalState field to the ouput.
Since (I think) Jackson 1.7 you can do this with a BeanSerializerModifier and extending BeanSerializerBase. I've tested the example below with Jackson 2.0.4.
import java.io.IOException;
import org.junit.Test;
import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
import com.fasterxml.jackson.databind.ser.impl.ObjectIdWriter;
import com.fasterxml.jackson.databind.ser.std.BeanSerializerBase;
public class JacksonSerializeWithExtraField {
#Test
public void testAddExtraField() throws Exception
{
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new SimpleModule() {
public void setupModule(SetupContext context) {
super.setupModule(context);
context.addBeanSerializerModifier(new BeanSerializerModifier() {
public JsonSerializer<?> modifySerializer(
SerializationConfig config,
BeanDescription beanDesc,
JsonSerializer<?> serializer) {
if (serializer instanceof BeanSerializerBase) {
return new ExtraFieldSerializer(
(BeanSerializerBase) serializer);
}
return serializer;
}
});
}
});
mapper.writeValue(System.out, new MyClass());
//prints {"classField":"classFieldValue","extraField":"extraFieldValue"}
}
class MyClass {
private String classField = "classFieldValue";
public String getClassField() {
return classField;
}
public void setClassField(String classField) {
this.classField = classField;
}
}
class ExtraFieldSerializer extends BeanSerializerBase {
ExtraFieldSerializer(BeanSerializerBase source) {
super(source);
}
ExtraFieldSerializer(ExtraFieldSerializer source,
ObjectIdWriter objectIdWriter) {
super(source, objectIdWriter);
}
ExtraFieldSerializer(ExtraFieldSerializer source,
String[] toIgnore) {
super(source, toIgnore);
}
protected BeanSerializerBase withObjectIdWriter(
ObjectIdWriter objectIdWriter) {
return new ExtraFieldSerializer(this, objectIdWriter);
}
protected BeanSerializerBase withIgnorals(String[] toIgnore) {
return new ExtraFieldSerializer(this, toIgnore);
}
public void serialize(Object bean, JsonGenerator jgen,
SerializerProvider provider) throws IOException,
JsonGenerationException {
jgen.writeStartObject();
serializeFields(bean, jgen, provider);
jgen.writeStringField("extraField", "extraFieldValue");
jgen.writeEndObject();
}
}
}
You can do this (previous version did not work with Jackson after 2.6, but this works with Jackson 2.7.3):
public static class CustomModule extends SimpleModule {
public CustomModule() {
addSerializer(CustomClass.class, new CustomClassSerializer());
}
private static class CustomClassSerializer extends JsonSerializer {
#Override
public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
//Validate.isInstanceOf(CustomClass.class, value);
jgen.writeStartObject();
JavaType javaType = provider.constructType(CustomClass.class);
BeanDescription beanDesc = provider.getConfig().introspect(javaType);
JsonSerializer<Object> serializer = BeanSerializerFactory.instance.findBeanSerializer(provider,
javaType,
beanDesc);
// this is basically your 'writeAllFields()'-method:
serializer.unwrappingSerializer(null).serialize(value, jgen, provider);
jgen.writeObjectField("my_extra_field", "some data");
jgen.writeEndObject();
}
}
}
Update:
I tried it out with Jackson 2.9.0 and 2.9.6 and it worked as expected with both. Perhaps try this out: http://jdoodle.com/a/z99 (run it locally - jdoodle apparently can't handle Jackson).
Though this question is already answered, I found another way that requires no special Jackson hooks.
static class JsonWrapper<T> {
#JsonUnwrapped
private T inner;
private String extraField;
public JsonWrapper(T inner, String field) {
this.inner = inner;
this.extraField = field;
}
public T getInner() {
return inner;
}
public String getExtraField() {
return extraField;
}
}
static class BaseClass {
private String baseField;
public BaseClass(String baseField) {
this.baseField = baseField;
}
public String getBaseField() {
return baseField;
}
}
public static void main(String[] args) throws JsonProcessingException {
Object input = new JsonWrapper<>(new BaseClass("inner"), "outer");
System.out.println(new ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(input));
}
Outputs:
{
"baseField" : "inner",
"extraField" : "outer"
}
For writing collections, you can simply use a view:
public static void main(String[] args) throws JsonProcessingException {
List<BaseClass> inputs = Arrays.asList(new BaseClass("1"), new BaseClass("2"));
//Google Guava Library <3
List<JsonWrapper<BaseClass>> modInputs = Lists.transform(inputs, base -> new JsonWrapper<>(base, "hello"));
System.out.println(new ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(modInputs));
}
Output:
[ {
"baseField" : "1",
"extraField" : "hello"
}, {
"baseField" : "2",
"extraField" : "hello"
} ]
Another and perhaps the most simple solution:
Make serialisation a 2-step process. First create a Map<String,Object> like:
Map<String,Object> map = req.mapper().convertValue( result, new TypeReference<Map<String,Object>>() {} );
then add the properties you want like:
map.put( "custom", "value" );
then serialise this to json:
String json = req.mapper().writeValueAsString( map );
For my use case, I could use a much simpler way. In a the base class I have for all my "Jackson Pojos" I add:
protected Map<String,Object> dynamicProperties = new HashMap<String,Object>();
...
public Object get(String name) {
return dynamicProperties.get(name);
}
// "any getter" needed for serialization
#JsonAnyGetter
public Map<String,Object> any() {
return dynamicProperties;
}
#JsonAnySetter
public void set(String name, Object value) {
dynamicProperties.put(name, value);
}
I can now deserialize to Pojo, work with fields and reserialize witjout losing any properties. I can also add/change non pojo properties:
// Pojo fields
person.setFirstName("Annna");
// Dynamic field
person.set("ex", "test");
(Got it from Cowtowncoder)
We can use reflection to get all the fields of the object you want to parse.
#JsonSerialize(using=CustomSerializer.class)
class Test{
int id;
String name;
String hash;
}
In custom serializer, we have our serialize method like this :
#Override
public void serialize(Test value, JsonGenerator jgen,
SerializerProvider provider) throws IOException,
JsonProcessingException {
jgen.writeStartObject();
Field[] fields = value.getClass().getDeclaredFields();
for (Field field : fields) {
try {
jgen.writeObjectField(field.getName(), field.get(value));
} catch (IllegalArgumentException | IllegalAccessException e) {
e.printStackTrace();
}
}
jgen.writeObjectField("extra_field", "whatever_value");
jgen.writeEndObject();
}
Inspired from what wajda said and written in this gist:
Here is how to add a listener for bean serialization in jackson 1.9.12. In this example, the listerner is considered as a Chain Of Command which interface is :
public interface BeanSerializerListener {
void postSerialization(Object value, JsonGenerator jgen) throws IOException;
}
MyBeanSerializer.java:
public class MyBeanSerializer extends BeanSerializerBase {
private final BeanSerializerListener serializerListener;
protected MyBeanSerializer(final BeanSerializerBase src, final BeanSerializerListener serializerListener) {
super(src);
this.serializerListener = serializerListener;
}
#Override
public void serialize(final Object bean, final JsonGenerator jgen, final SerializerProvider provider) throws IOException, JsonGenerationException {
jgen.writeStartObject();
if (_propertyFilterId != null) {
serializeFieldsFiltered(bean, jgen, provider);
} else {
serializeFields(bean, jgen, provider);
}
serializerListener.postSerialization(bean, jgen);
jgen.writeEndObject();
}
}
MyBeanSerializerBuilder.java:
public class MyBeanSerializerBuilder extends BeanSerializerBuilder {
private final BeanSerializerListener serializerListener;
public MyBeanSerializerBuilder(final BasicBeanDescription beanDesc, final BeanSerializerListener serializerListener) {
super(beanDesc);
this.serializerListener = serializerListener;
}
#Override
public JsonSerializer<?> build() {
BeanSerializerBase src = (BeanSerializerBase) super.build();
return new MyBeanSerializer(src, serializerListener);
}
}
MyBeanSerializerFactory.java:
public class MyBeanSerializerFactory extends BeanSerializerFactory {
private final BeanSerializerListener serializerListener;
public MyBeanSerializerFactory(final BeanSerializerListener serializerListener) {
super(null);
this.serializerListener = serializerListener;
}
#Override
protected BeanSerializerBuilder constructBeanSerializerBuilder(final BasicBeanDescription beanDesc) {
return new MyBeanSerializerBuilder(beanDesc, serializerListener);
}
}
The last class below shows how to provide it using Resteasy 3.0.7:
#Provider
public class ObjectMapperProvider implements ContextResolver<ObjectMapper> {
private final MapperConfigurator mapperCfg;
public ObjectMapperProvider() {
mapperCfg = new MapperConfigurator(null, null);
mapperCfg.setAnnotationsToUse(new Annotations[]{Annotations.JACKSON, Annotations.JAXB});
mapperCfg.getConfiguredMapper().setSerializerFactory(serializerFactory);
}
#Override
public ObjectMapper getContext(final Class<?> type) {
return mapperCfg.getConfiguredMapper();
}
}
We can extend BeanSerializer, but with little trick.
First, define a java class to wrapper your POJO.
#JsonSerialize(using = MixinResultSerializer.class)
public class MixinResult {
private final Object origin;
private final Map<String, String> mixed = Maps.newHashMap();
#JsonCreator
public MixinResult(#JsonProperty("origin") Object origin) {
this.origin = origin;
}
public void add(String key, String value) {
this.mixed.put(key, value);
}
public Map<String, String> getMixed() {
return mixed;
}
public Object getOrigin() {
return origin;
}
}
Then,implement your custom serializer.
public final class MixinResultSerializer extends BeanSerializer {
public MixinResultSerializer() {
super(SimpleType.construct(MixinResult.class), null, new BeanPropertyWriter[0], new BeanPropertyWriter[0]);
}
public MixinResultSerializer(BeanSerializerBase base) {
super(base);
}
#Override
protected void serializeFields(Object bean, JsonGenerator gen, SerializerProvider provider) throws IOException {
if (bean instanceof MixinResult) {
MixinResult mixin = (MixinResult) bean;
Object origin = mixin.getOrigin();
BeanSerializer serializer = (BeanSerializer) provider.findValueSerializer(SimpleType.construct(origin.getClass()));
new MixinResultSerializer(serializer).serializeFields(origin, gen, provider);
mixin.getMixed().entrySet()
.stream()
.filter(entry -> entry.getValue() != null)
.forEach((entry -> {
try {
gen.writeFieldName(entry.getKey());
gen.writeRawValue(entry.getValue());
} catch (IOException e) {
throw new RuntimeException(e);
}
}));
} else {
super.serializeFields(bean, gen, provider);
}
}
}
This way, we can handle the case that origin object using jackson annotations to custom serialize behavior.
I needed this ability as well; in my case, to support field expansion on REST services. I ended up developing a tiny framework to solve this problem, and it's open sourced on github. It's also available in the maven central repository.
It takes care of all the work. Simply wrap the POJO in a MorphedResult, and then add or remove properties at will. When serialized, the MorphedResult wrapper disappears and any 'changes' appear in the serialized JSON object.
MorphedResult<?> result = new MorphedResult<>(pojo);
result.addExpansionData("my_extra_field", "some data");
See the github page for more details and examples. Be sure to register the libraries 'filter' with Jackson's object mapper like so:
ObjectMapper mapper = new ObjectMapper();
mapper.setFilters(new FilteredResultProvider());
This google groups thread points to the BeanSerializerModifier.changeProperties method:
https://groups.google.com/g/jackson-user/c/uYIxbRZhsIM/m/1QpLh7G72C0J
It looks like this method makes the least interference with the object serialization, which is very convenient if you have other serialization customizations.
You can add more objects to the given beanProperties list.
Suppose, we have this bean to be serialized:
public class MyClass {
private final String name;
private final String description;
public MyClass(String name, String description) {
this.name = name;
this.description = description;
}
#JsonProperty(access = JsonProperty.Access.READ_ONLY)
public String getName() {
return name;
}
#JsonProperty(access = JsonProperty.Access.READ_ONLY)
public String getDescription() {
return description;
}
}
Then you can add a SerializerModifier to your ObjectMapper instance.
The most interesting parts are the MyBeanSerializerModifier.changeProperties and the CustomPropertyWriter.value methods.
private void addSerializationCustomization(ObjectMapper objectMapper,
SomeAdditionalDataFactory dataFactory) {
SimpleModule module = new SimpleModule();
BeanSerializerModifier modifier = new MyBeanSerializerModifier(dataFactory);
module.setSerializerModifier(modifier);
objectMapper.registerModule(module);
}
private static class MyBeanSerializerModifier extends BeanSerializerModifier {
private final SomeAdditionalDataFactory dataFactory;
public MyBeanSerializerModifier(SomeAdditionalDataFactory dataFactory) {
this.dataFactory = dataFactory;
}
#Override
public List<BeanPropertyWriter> changeProperties(SerializationConfig config,
BeanDescription beanDesc,
List<BeanPropertyWriter> beanProperties) {
if (MyClass.class.isAssignableFrom(beanDesc.getBeanClass())) {
Map<String, Function<MyClass, String>> additionalFields = Map.of(
"someData1",
myObj -> dataFactory.getSomeData1(myObj),
"someData2",
myObj -> dataFactory.getSomeData2(myObj),
"someData3",
myObj -> dataFactory.getSomeData3(myObj)
);
JavaType javaType = SimpleType.constructUnsafe(String.class);
for (Map.Entry<String, Function<MyClass, String>> entry : additionalFields.entrySet()) {
VirtualAnnotatedMember member = new VirtualAnnotatedMember(
null, beanDesc.getBeanClass(), entry.getKey(), javaType);
BeanPropertyDefinition definition = SimpleBeanPropertyDefinition
.construct(config, member, new PropertyName(entry.getKey()));
BeanPropertyWriter writer = new CustomPropertyWriter<>(
definition, javaType, entry.getValue());
beanProperties.add(writer);
}
}
return super.changeProperties(config, beanDesc, beanProperties);
}
}
private static class CustomPropertyWriter<T> extends VirtualBeanPropertyWriter {
private final Function<T, String> getter;
public CustomPropertyWriter(BeanPropertyDefinition propDef,
JavaType declaredType,
Function<T, String> getter) {
super(propDef, null, declaredType);
this.getter = getter;
}
#Override
#SuppressWarnings("unchecked")
protected Object value(Object bean,
JsonGenerator gen,
SerializerProvider prov) throws Exception {
return getter.apply((T) bean);
}
#Override
public VirtualBeanPropertyWriter withConfig(MapperConfig<?> config,
AnnotatedClass declaringClass,
BeanPropertyDefinition propDef,
JavaType type) {
throw new IllegalStateException("Should not be called on this type");
}
}
After looking more on the Jackson source code I concluded that it's simply impossible to achieve without writing my own BeanSerializer, BeanSerializerBuilder and BeanSerializerFactory and provide some extension points like:
/*
/**********************************************************
/* Extension points
/**********************************************************
*/
protected void beforeEndObject(T bean, JsonGenerator jgen, SerializerProvider provider) throws IOException, JSONException {
// May be overridden
}
protected void afterStartObject(T bean, JsonGenerator jgen, SerializerProvider provider) throws IOException, JSONException {
// May be overridden
}
Unfortunately I had to copy and paste entire Jackson's BeanSerializer source code to MyCustomBeanSerializer because the former is not developed for extensions declaring all the fields and some important methods (like serialize(...)) as final