Is there a simple way to serialize an object using Jackson to base64 encoded JSON? (object -> JSON -> base64)
I tried using a custom StdSerializer, but this (of course) results in a endless loop:
class MySerializer extends StdSerializer<Foo> {
public void serialize(Foo value, JsonGenerator gen, SerializerProvider provider) {
StringWriter stringWriter = new StringWriter();
JsonGenerator newGen = gen.getCodec().getFactory().createGenerator(stringWriter);
gen.getCodec().getFactory().getCodec().writeValue(newGen, value);
String json = stringWriter.toString();
String base64 = new String(Base64.getEncoder().encode(json.getBytes()));
gen.writeString(base64);
}
}
A workaround is to copy all fields to another class and use that class for the intermediate representation:
class TmpFoo {
public String field1;
public int field2;
// ...
}
class MySerializer extends StdSerializer<Foo> {
public void serialize(Foo value, JsonGenerator gen, SerializerProvider provider) {
TmpFoo tmp = new TmpFoo();
tmp.field1 = value.field1;
tmp.field2 = value.field2;
// etc.
StringWriter stringWriter = new StringWriter();
JsonGenerator newGen = gen.getCodec().getFactory().createGenerator(stringWriter);
gen.getCodec().getFactory().getCodec().writeValue(newGen, tmp); // here "tmp" instead of "value"
String json = stringWriter.toString();
String base64 = new String(Base64.getEncoder().encode(json.getBytes()));
gen.writeString(base64);
}
}
Creating a new ObjectMapper is not desired, because I need all registered modules and serializers of the default ObjectMapper.
I was hoping for some easier way of achieving this.
EDIT: Example
Step 1: Java Object
class Foo {
String field1 = "foo";
int field2 = 42;
}
Step 2: JSON
{"field1":"foo","field2":42}
Step 3: Base64
eyJmaWVsZDEiOiJmb28iLCJmaWVsZDIiOjQyfQ==
According to this site, there is a workaround to avoid this recursion problem:
When we define a custom serializer, Jackson internally overrides the
original BeanSerializer instance [...] our SerializerProvider finds
the customized serializer every time, instead of the default one, and
this causes an infinite loop.
A possible workaround is using BeanSerializerModifier to store the
default serializer for the type Folder before Jackson internally
overrides it.
If I understood the workaround correctly, your Serializer should look like this:
class FooSerializer extends StdSerializer<Foo> {
private final JsonSerializer<Object> defaultSerializer;
public FooSerializer(JsonSerializer<Object> defaultSerializer) {
super(Foo.class);
this.defaultSerializer = defaultSerializer;
}
#Override
public void serialize(Foo value, JsonGenerator gen, SerializerProvider provider) throws IOException {
StringWriter stringWriter = new StringWriter();
JsonGenerator tempGen = provider.getGenerator().getCodec().getFactory().createGenerator(stringWriter);
defaultSerializer.serialize(value, tempGen, provider);
tempGen.flush();
String json = stringWriter.toString();
String base64 = new String(Base64.getEncoder().encode(json.getBytes()));
gen.writeString(base64);
}
}
In addition to the serializer, a modifier is needed:
public class FooBeanSerializerModifier extends BeanSerializerModifier {
#Override
public JsonSerializer<?> modifySerializer(
SerializationConfig config, BeanDescription beanDesc, JsonSerializer<?> serializer) {
if (beanDesc.getBeanClass().equals(Foo.class)) {
return new FooSerializer((JsonSerializer<Object>) serializer);
}
return serializer;
}
}
Example module:
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.setSerializerModifier(new FooBeanSerializerModifier());
mapper.registerModule(module);
EDIT:
I've added flush() to flush the JsonGenerator tempGen.
Also, I've created a minimal test enviroment with JUnit, which verifies your Example with Foo: The github repo can be found here.
EDIT: Alternative 2
Another (simple) option is using a wrapper class with generics:
public class Base64Wrapper<T> {
private final T wrapped;
private Base64Wrapper(T wrapped) {
this.wrapped = wrapped;
}
public T getWrapped() {
return this.wrapped;
}
public static <T> Base64Wrapper<T> of(T wrapped) {
return new Base64Wrapper<>(wrapped);
}
}
public class Base64WrapperSerializer extends StdSerializer<Base64Wrapper> {
public Base64WrapperSerializer() {
super(Base64Wrapper.class);
}
#Override
public void serialize(Base64Wrapper value, JsonGenerator gen, SerializerProvider provider) throws IOException {
StringWriter stringWriter = new StringWriter();
JsonGenerator tempGen = provider.getGenerator().getCodec().getFactory().createGenerator(stringWriter);
provider.defaultSerializeValue(value.getWrapped(), tempGen);
tempGen.flush();
String json = stringWriter.toString();
String base64 = new String(Base64.getEncoder().encode(json.getBytes()));
gen.writeString(base64);
}
}
An example usecase would be:
final ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addSerializer(new Base64WrapperSerializer());
mapper.registerModule(module);
final Foo foo = new Foo();
final Base64Wrapper<Foo> base64Wrapper = Base64Wrapper.of(foo);
final String base64Json = mapper.writeValueAsString(base64Wrapper);
This example can be found in this GitHub (branch: wrapper) repo, verifing you BASE64 String from your foo example with JUnit testing.
Instead of creating new object you may convert existing one into map. Like in the example below
import static java.nio.charset.StandardCharsets.UTF_8;
public class FooSerializer extends StdSerializer<Foo> {
public FooSerializer() {
super(Foo.class);
}
#Override
public void serialize(Foo foo, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) {
try {
ObjectMapper mapper = (ObjectMapper) jsonGenerator.getCodec();
var map = toMap(foo); // if you need class info for deserialization than use toMapWithClassInfo
String json = mapper.writeValueAsString(map);
jsonGenerator.writeString(Base64.getEncoder().encodeToString(json.getBytes(UTF_8)));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static Map<String, Object> toMap(Object o) throws Exception {
Map<String, Object> result = new HashMap<>();
Field[] declaredFields = o.getClass().getDeclaredFields();
for (Field field : declaredFields) {
field.setAccessible(true);
result.put(field.getName(), field.get(o));
}
return result;
}
public static Map<String, Object> toMapWithClassInfo(Object obj) throws Exception {
Map<String, Object> result = new HashMap<>();
BeanInfo info = Introspector.getBeanInfo(obj.getClass());
for (PropertyDescriptor pd : info.getPropertyDescriptors()) {
Method reader = pd.getReadMethod();
if (reader != null)
result.put(pd.getName(), reader.invoke(obj));
}
return result;
}
}
I'm providing 2 ways of converting into map: with and without class info. Choose the one, applicable to your problem.
To serialize object jackson search #JsonValue method. You can add encodedJsonString method annotated by #JsonValue in Foo class.
Try with this:
#Getter
#Setter
public class Foo implements Serializable {
private static final long serialVersionUID = 1L;
public String field1;
public int field2;
#JsonValue
public String toEncodedJsonString() {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
new ObjectOutputStream(baos).writeObject(this);
return org.apache.commons.codec.binary.Base64.encodeBase64String(baos.toByteArray());
}catch (Exception ex){
}
return null;
}
}
I receive data in JSON
{
"status": "INVALID_DATA",
"errors":{ "invalid_id": "Id isn't available",
...
"wrong_address": "Address error msg"
}
}
Keys and their quantity in structure "errors" are unknown for me. I'm trying to map this with class
#JsonIgnoreProperties(ignoreUnknown = true)
public class StatusErrors
{
private String status;
private HashMap<String, String> errors = new HashMap<String, String>();
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public HashMap<String, String> getErrors() {
return errors;
}
public void setErrors(HashMap<String, String> errors) {
this.errors = errors;
}
}
It works fine if I have "errors", but when server say "OK" and has no errors it send me
{
"status": "OK",
"errors":[]
}
(Don't ask me who write the server)
So mapper crashes.
I'm trying to write a custom JsonDeserializer (generic way)
public abstract class ExcludeEmptyArrayDeserializer<T> extends JsonDeserializer<T> {
private final Class<T> clazz;
protected ExcludeEmptyArrayDeserializer(Class<T> clazz) {
this.clazz = clazz;
}
#Override
public T deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
ObjectCodec oc = jp.getCodec();
JsonNode node = oc.readTree(jp);
if(node.has("errors")) {
if (node.get("errors").isArray() && !node.get("errors").getElements().hasNext())
((ObjectNode)node).remove("errors");
}
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readValue(node, clazz); // doesn't work
//return oc.treeToValue(node, clazz); // doesn't work too
}
}
public class StatusErrorsDeserializer extends ExcludeEmptyArrayDeserializer<StatusErrors> {
public StatusErrorsDeserializer() {
super(StatusErrors.class);
}
}
The result usage code has a view
SimpleModule module = new SimpleModule("", Version.unknownVersion());
module.addDeserializer(StatusErrors.class, new StatusErrorsDeserializer());
ObjectMapper mapper = new ObjectMapper().setVisibility(JsonMethod.FIELD, JsonAutoDetect.Visibility.ANY).withModule(module);
MappingJacksonHttpMessageConverter messageConverter = new MappingJacksonHttpMessageConverter();
messageConverter.setObjectMapper(mapper);
getRestTemplate().getMessageConverters().clear();
getRestTemplate().getMessageConverters().add(messageConverter);
The "errors" node deleted correctly but this solution still doesn't work.
I suppose I make a mistake in JsonDeserializer.deserialize method but don't get an idea.
BTW StatusErrors class can be a base class for other complicated messages from server.
The easiest solution is to change errors variable declaration to Object:
#JsonIgnoreProperties(ignoreUnknown = true)
class StatusErrors {
private String status;
private Object errors;
public Map<String, String> getErrorsMap() {
if (this.errors instanceof Map) {
return (Map)this.errors;
}
return null;
}
....
You don't need any serializers and deserializers:
ObjectMapper objectMapper = new ObjectMapper();
StatusErrors result1 = objectMapper.readValue(JSON1, StatusErrors.class);
System.out.println(result1);
System.out.println(result1.getErrors().getClass());
System.out.println(result1.getErrorsMap());
StatusErrors result2 = objectMapper.readValue(JSON2, StatusErrors.class);
System.out.println(result2);
The code above will print:
StatusErrors(status=INVALID_DATA, errors={invalid_id=Id isn't available, wrong_address=Address error msg})
class java.util.LinkedHashMap
{invalid_id=Id isn't available, wrong_address=Address error msg}
StatusErrors(status=OK, errors=[])
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);
}
}
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
I would like serialize an object such that one of the fields will be named differently based on the type of the field. For example:
public class Response {
private Status status;
private String error;
private Object data;
[ getters, setters ]
}
Here, I would like the field data to be serialized to something like data.getClass.getName() instead of always having a field called data which contains a different type depending on the situation.
How might I achieve such a trick using Jackson?
I had a simpler solution using #JsonAnyGetter annotation, and it worked like a charm.
import java.util.Collections;
import java.util.Map;
public class Response {
private Status status;
private String error;
#JsonIgnore
private Object data;
[getters, setters]
#JsonAnyGetter
public Map<String, Object> any() {
//add the custom name here
//use full HashMap if you need more than one property
return Collections.singletonMap(data.getClass().getName(), data);
}
}
No wrapper needed, no custom serializer needed.
Using a custom JsonSerializer.
public class Response {
private String status;
private String error;
#JsonProperty("p")
#JsonSerialize(using = CustomSerializer.class)
private Object data;
// ...
}
public class CustomSerializer extends JsonSerializer<Object> {
public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
jgen.writeStartObject();
jgen.writeObjectField(value.getClass().getName(), value);
jgen.writeEndObject();
}
}
And then, suppose you want to serialize the following two objects:
public static void main(String... args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
Response r1 = new Response("Error", "Some error", 20);
System.out.println(mapper.writeValueAsString(r1));
Response r2 = new Response("Error", "Some error", "some string");
System.out.println(mapper.writeValueAsString(r2));
}
The first one will print:
{"status":"Error","error":"Some error","p":{"java.lang.Integer":20}}
And the second one:
{"status":"Error","error":"Some error","p":{"java.lang.String":"some string"}}
I have used the name p for the wrapper object since it will merely serve as a placeholder. If you want to remove it, you'd have to write a custom serializer for the entire class, i.e., a JsonSerializer<Response>.
my own solution.
#Data
#EqualsAndHashCode
#ToString
#JsonSerialize(using = ElementsListBean.CustomSerializer.class)
public class ElementsListBean<T> {
public ElementsListBean()
{
}
public ElementsListBean(final String fieldName, final List<T> elements)
{
this.fieldName = fieldName;
this.elements = elements;
}
private String fieldName;
private List<T> elements;
public int length()
{
return (this.elements != null) ? this.elements.size() : 0;
}
private static class CustomSerializer extends JsonSerializer<Object> {
public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException,
JsonProcessingException
{
if (value instanceof ElementsListBean) {
final ElementsListBean<?> o = (ElementsListBean<?>) value;
jgen.writeStartObject();
jgen.writeArrayFieldStart(o.getFieldName());
for (Object e : o.getElements()) {
jgen.writeObject(e);
}
jgen.writeEndArray();
jgen.writeNumberField("length", o.length());
jgen.writeEndObject();
}
}
}
}
You can use the annotation JsonTypeInfo, which tell Jackson exactly that and you don't need to write a custom serializer. There's various way to include this information, but for your specific question you'd use As.WRAPPER_OBJECT and Id.CLASS. For example:
public static class Response {
private Status status;
private String error;
#JsonTypeInfo(include = As.WRAPPER_OBJECT, use = Id.CLASS)
private Object data;
}
This, however, will not work on primitive type, such as a String or Integer. You don't need that information for primitives anyways, since they are natively represented in JSON and Jackson knows how to handle them. The added bonus with using the annotation is that you get deserialization for free, if you ever need it. Here's an example:
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
Response r1 = new Response("Status", "An error", "some data");
Response r2 = new Response("Status", "An error", 10);
Response r3 = new Response("Status", "An error", new MyClass("data"));
System.out.println(mapper.writeValueAsString(r1));
System.out.println(mapper.writeValueAsString(r2));
System.out.println(mapper.writeValueAsString(r3));
}
#JsonAutoDetect(fieldVisibility=Visibility.ANY)
public static class MyClass{
private String data;
public MyClass(String data) {
this.data = data;
}
}
and the result:
{"status":"Status","error":"An error","data":"some data"}
{"status":"Status","error":"An error","data":10}
{"status":"Status","error":"An error","data":{"some.package.MyClass":{"data":"data"}}}
Based on #tlogbon response,
Here is my solution to wrap a List of Items with a specific/dynamic filed name
public class ListResource<T> {
#JsonIgnore
private List<T> items;
#JsonIgnore
private String fieldName;
public ListResource(String fieldName, List<T> items) {
this.items = items;
this.fieldName = fieldName;
}
#JsonAnyGetter
public Map<String, List<T>> getMap() {
return Collections.singletonMap(fieldName, items);
}