Serialize a custom map with Jackson - java

I want to serialize a custom Map to JSON.
The class with implements the map interface is the following:
public class MapImpl extends ForwardingMap<String, String> {
//ForwardingMap comes from Guava
private String specialInfo;
private HashMap<String, String> delegate;
#Override
protected Map<String, String> delegate() {
return this.delegate;
}
// some getters....
}
If I call now
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(new File("/somePath/myJson.json"), objectOfMapImpl);
Jackson will serialize the map and ignores the variable specialInfo
I tried some things with a custom implementation of JsonSerializer but I ended up with this snippet:
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule("someModule");
module.addSerializer(CheapestResponseDates.class, new JsonSerializer<MapImpl>() {
#Override
public void serialize(final MapImpl value, final JsonGenerator jgen, final SerializerProvider provider) throws IOException, JsonProcessingException {
CheapestResponseDurations.class);
// how to serialize the map here? maybe be in a data node...
jgen.writeStartObject();
jgen.writeObjectField("info", value.getInfo());
jgen.writeEndObject();
}
});
mapper.registerModule(module);
I am using JDK 1.7 and Jackson 2.3.1

You can leverage the #JsonAnySetter/ #JsonAnyGetter annotations, as described in this blog post. Since, as you mentioned, your custom map class must implement a Map interface, you could extract a separate "bean" interface and tell Jackson to use it instead when serializing via #JsonSerialize(as = ...) annotation.
I've slightly modified you example to illustrate how it could work. Note that if you want to deserialize the json string back to your map object, you may need to do some other tricks.
public class MapSerialize {
public static interface MyInterface {
String getSpecialInfo();
#JsonAnyGetter
Map<String, String> delegate();
}
#JsonSerialize(as = MyInterface.class)
public static class MyImpl extends ForwardingMap<String, String> implements MyInterface {
private String specialInfo;
private HashMap<String, String> delegate = new HashMap<String, String>();
public Map<String, String> delegate() {
return this.delegate;
}
#Override
public String getSpecialInfo() {
return specialInfo;
}
public void setSpecialInfo(String specialInfo) {
this.specialInfo = specialInfo;
}
#Override
public String put(String key, String value) {
return delegate.put(key, value);
}
}
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
MyImpl objectOfMapImpl = new MyImpl();
objectOfMapImpl.setSpecialInfo("specialInfo");
objectOfMapImpl.put("XXX", "YYY");
String json = mapper.writeValueAsString(objectOfMapImpl);
System.out.println(json);
}
}

Related

Jackson - How to replace serialized Map

I need to serialize a graph to JSON containing List and Map. Each map instance contains a UUID field. The graph can contain more than one Map instance with the same UUID. Maps with the same UUID are considered identical.
During Serialization, I would like to replace map instances that have a previously been serialized by only their UUID.
What is the best way to achieve that with Jackson?
Thanks
You can implement a custom serializer for your graph class.
You have to extend StdSerializer and override
#Override
public void serialize(T value, JsonGenerator jgen, SerializerProvider provider)
throws IOException, JsonProcessingException
When you did that you need to let jackson know about your serializer. You can achieve that by annotate your graph class with #JsonSerialize(using = CustomSerializer.class) or you could register a new module containing the custom serializer.
Below is the working solution I came up with.
However, is there a more elegant way to get a lifecycle hook on top-level serialize calls (which is needed to re-init the custom serializer)?
Also, I'm not convinced that keeping track of visited objects per thread, using ThreadLocal, is the best solution. Any advices?
Thanks
public class IdentifiableSerializerTest {
public static void main(String[] args) throws JsonProcessingException {
ObjectMapper mapper = createObjectMapper();
test(mapper);
}
interface Identifiable {
Long getId();
}
public static ObjectMapper createObjectMapper() {
ObjectMapper mapper = new ObjectMapper();
// disable quoting - for testing purpose
mapper.configure(JsonGenerator.Feature.QUOTE_FIELD_NAMES, false);
mapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
// register serializer for Identifiable type
SimpleModule module = new SimpleModule();
module.addSerializer(Identifiable.class, new IdentifiableSerializer(mapper.writer()));
mapper.registerModule(module);
// lifecycle hook to re-init IdentifiableSerializer on root-level serialize calls
mapper.setSerializerProvider(new IdentifiableSerializerProvider());
return mapper;
}
/**
* This class serves to intercept root-level serialize calls in order to
* clean the map of visited objects, see {#link IdentifiableSerializer#visited}.
*
* TODO: this seems lot of code just to get a hook on root-level serialize calls...
*/
public static class IdentifiableSerializerProvider extends DefaultSerializerProvider {
public IdentifiableSerializerProvider() { super(); }
protected IdentifiableSerializerProvider(SerializerProvider src, SerializationConfig config, SerializerFactory f) {
super(src, config, f);
}
#Override
public DefaultSerializerProvider createInstance(SerializationConfig config, SerializerFactory f) {
return new IdentifiableSerializerProvider(this, config, f);
}
#Override
public void serializeValue(JsonGenerator gen, Object value) throws IOException {
IdentifiableSerializer.reset();
super.serializeValue(gen, value);
}
}
public static class IdentifiableSerializer extends JsonSerializer<Identifiable> {
private static ThreadLocal<Set> visited = new ThreadLocal<Set>() {
#Override
protected Set initialValue() {
return new HashSet();
}
};
public static void reset() {
visited.get().clear();
}
private final ObjectWriter delegate;
public IdentifiableSerializer(ObjectWriter delegate) {
this.delegate = delegate;
}
#Override
public void serialize(Identifiable value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
Long id = value.getId();
Set seen = visited.get();
if (seen.contains(id)) {
jgen.writeStartObject();
jgen.writeNumberField("#REF", id);
jgen.writeEndObject();
}
else {
seen.add(id);
delegate.writeValue(jgen, value);
}
}
}
static class IdentifiableMap extends HashMap implements Identifiable {
static long counter = 0;
Long id = counter++;
{
put("#ID", id);
}
#Override
public Long getId() {
return id;
}
}
public static void test(ObjectMapper mapper) throws JsonProcessingException {
Map myMap = new IdentifiableMap() {{
put("key1", 1);
put("key2", 2);
put("key3", 3);
}};
List<Map> myList = Arrays.asList(myMap, myMap);
String expected = "[{key1:1,key2:2,key3:3,#ID:0},{#REF:0}]";
String actual = mapper.writeValueAsString(myList);
Assert.assertEquals(expected, actual);
System.out.println("SUCCESS");
}
}

Jackson - serialization of Map<String, Object> implementation

I have following class:
public class Some implements Map<String, Object>{
private Map<String, Object> innerMap;
//implementation that can only set innerMap in constructor and cannot add or remove values
}
The problem is that I cannot deserialize this in jackson correctly. If I serialize without default typing, it is OK, since it is serialized as {"one":"two"} and deserialized correctly (I had to implement deserializer with
return new Some(jp.readValueAs(new TypeReference<HashMap<String,Object>>(){}));
When I use default typing turned on, this is serialized as
["com.class.Some",{"one":"two"}]
But deserialization is throwing
com.fasterxml.jackson.databind.JsonMappingException: Unexpected token (START_OBJECT), expected START_ARRAY: need JSON Array to contain As.WRAPPER_ARRAY type information for class java.util.HashMap
Any thoughts?
Annotate your constructor with #JsonCreator:
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
Some some = new Some(new HashMap<String, Object>() {{put("a", 1);}});
String json = mapper.writeValueAsString(some);
System.out.println("serialized : " + json);
some = mapper.readValue(json, Some.class);
System.out.println("deserialized: " + some);
}
// Read only delegating Map
public static class Some extends AbstractMap<String, Object> {
private Map<String, Object> delegate;
#JsonCreator
public Some(Map<String, Object> delegate) {
this.delegate = Collections.unmodifiableMap(delegate);
}
#Override
public Set<Entry<String, Object>> entrySet() {
return delegate.entrySet();
}
}
This is what I needed - custom deserializer:
public class SomeDeserializer extends JsonDeserializer<Some> {
#Override
public Object deserializeWithType(JsonParser jsonParser, DeserializationContext ctxt, TypeDeserializer typeDeserializer) throws IOException {
return typeDeserializer.deserializeTypedFromObject(jsonParser, ctxt);
}
#SuppressWarnings("unchecked")
#Override
public Some deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException {
JsonDeserializer<Object> deserializer = ctxt.findRootValueDeserializer(
ctxt.getTypeFactory().constructMapType(Map.class, String.class, Object.class));
return new Some((Map) deserializer.deserialize(jp, ctxt, new HashMap<>()));
}
}

How to apply a Jackson SerializationFeature on one field?

Given the following POJO, I would like to apply SerializationFeature.WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED on the links field ONLY.
public class HalRepresentation {
#JsonProperty("_links")
#JsonInclude(JsonInclude.Include.NON_DEFAULT)
private final Map<String, List<Link>> links = new HashMap<String, List<Link>>();
#JsonProperty("_embedded")
#JsonInclude(JsonInclude.Include.NON_DEFAULT)
private final Map<String, Object> embedded = new HashMap<String, Object>();
protected HalRepresentation() {
}
public Map<String, List<Link>> getLinks() {
return links;
}
public Map<String, Object> getEmbedded() {
return embedded;
}
}
I tried to serialize it as follows:
ObjectMapper objectMapper = new ObjectMapper()
.enable(SerializationFeature.WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED);
try {
outputStream.write(objectMapper.writeValueAsBytes(halRepresentation));
outputStream.flush();
} catch (JsonProcessingException e) {
throw new IllegalStateException(e);
}
But when I do this the unwrap feature is also applied on the embedded field. I tried to find an equivalent annotation for WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED, but I can't find one. Do you have an idea for this using Jackson ?
As stated by #AlexeyGavrilov, it does not seem to be possible: https://stackoverflow.com/a/29133209/1225328. A workaround could be to create a custom JsonSerializer:
public class SingleElementCollectionsUnwrapper extends JsonSerializer<Object> {
#Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException, JsonProcessingException {
if (!serializers.getConfig().isEnabled(SerializationFeature.WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED)) {
new ObjectMapper().enable(SerializationFeature.WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED).writeValue(gen, value);
} else {
gen.writeObject(value);
}
}
}
Then, annotate the links field with #JsonSerialize:
#JsonProperty("_links")
#JsonInclude(JsonInclude.Include.NON_DEFAULT)
#JsonSerialize(using = SingleElementCollectionsUnwrapper.class)
private final Map<String, List<Link>> links = new HashMap<String, List<Link>>();

How to serialize with Jackson a java.util.Map based class

I have a class which looks like this:
#JsonFormat(shape=JsonFormat.Shape.OBJECT)
public class MyMap implements Map<String, String>
{
protected Map<String, String> myMap = new HashMap<String, String>();
protected String myProperty = "my property";
public String getMyProperty()
{
return myProperty;
}
public void setMyProperty(String myProperty)
{
this.myProperty = myProperty;
}
//
// java.util.Map mathods implementations
// ...
}
And a main method with this code:
MyMap map = new MyMap();
map.put("str1", "str2");
ObjectMapper mapper = new ObjectMapper();
mapper.getDeserializationConfig().withAnnotationIntrospector(new JacksonAnnotationIntrospector());
mapper.getSerializationConfig().withAnnotationIntrospector(new JacksonAnnotationIntrospector());
System.out.println(mapper.writeValueAsString(map));
When executing this code I'm getting the following output: {"str1":"str2"}
My question is why the internal property "myProperty" is not serialized with the map?
What should be done to serialize internal properties?
Most probably you will end up with implementing your own serializer which will handle your custom Map type. Please refer to this question for more information.
If you choose to replace inheritance with composition, that is to make your class to include a map field not to extend a map, then it is pretty easy to solve this using the #JsonAnyGetter annotation.
Here is an example:
public class JacksonMap {
public static class Bean {
private final String field;
private final Map<String, Object> map;
public Bean(String field, Map<String, Object> map) {
this.field = field;
this.map = map;
}
public String getField() {
return field;
}
#JsonAnyGetter
public Map<String, Object> getMap() {
return map;
}
}
public static void main(String[] args) throws JsonProcessingException {
Bean map = new Bean("value1", Collections.<String, Object>singletonMap("key1", "value2"));
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.writeValueAsString(map));
}
}
Output:
{"field":"value1","key1":"value2"}

Jackson: How to add custom property to the JSON without modifying the POJO

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

Categories

Resources