Jackson encode all properties to base64 - java

I have more than one class that I want to serialize using Jackson to generate Json, for example
public class A{
int id;
String name;
Object database;
... getter and setter
}
I need to encode all value of the json to base64, so I configure the object mapper like this
public class Base64Serializer<T> extends StdSerializer<T> {
private static final long serialVersionUID = 1L;
protected Base64Serializer(Class<?> t, boolean f) {
super(t, f);
}
#Override
public void serialize(T value, JsonGenerator jsonGenerator, SerializerProvider arg2) throws IOException {
String ecnodedOutput = Base64.getEncoder().encodeToString(((String) value).getBytes());
jsonGenerator.writeString(ecnodedOutput);
}
}
//Using the base64 Serializer to configure Object mapper
SimpleModule module = new SimpleModule();
module.addSerializer(new Base64Serializer(String.class, false));
objectMapper.registerModule(module);
objectMapper.writerWithDefaultPrettyPrinter().writeValue(outputStream,intanceOfClassA);
The problem is it's only encode the String type as the serializer only accept one type, is there any method to encode all field values, (long, int, object, etc..) I mean to encode all value of the json field regarding it type of string or int??

You could use following:
Serializer
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.Base64;
public class Base64Serializer<T extends Serializable> extends StdSerializer<T> {
private static final long serialVersionUID = 1L;
protected Base64Serializer(Class<?> t, boolean f) {
super(t, f);
}
#Override
public void serialize(T value, JsonGenerator jsonGenerator, SerializerProvider arg2) throws IOException {
String ecnodedOutput = Base64.getEncoder().encodeToString(serialize(value));
jsonGenerator.writeString(ecnodedOutput);
}
public static byte[] serialize(Object obj) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream os = new ObjectOutputStream(out);
os.writeObject(obj);
return out.toByteArray();
}
}
Registration and test:
public class SerializerTest {
public static void main(String[] args) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
addSerializers(module, Serializable.class, int.class, double.class, float.class, char.class, byte.class, short.class);
objectMapper.registerModule(module);
System.out.println(objectMapper.writeValueAsString(new A(10, "test", Arrays.asList(10000L, "TTTT2"))));
}
private static void addSerializers(SimpleModule module, Class... classes) {
Arrays.stream(classes).forEach(c -> module.addSerializer(new Base64Serializer(c, false)));
}
}
Output:
{
"id": "rO0ABXNyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAAK",
"name": "rO0ABXQABHRlc3Q=",
"database": "rO0ABXNyABpqYXZhLnV0aWwuQXJyYXlzJEFycmF5TGlzdNmkPL7NiAbSAgABWwABYXQAE1tMamF2YS9sYW5nL09iamVjdDt4cHVyABdbTGphdmEuaW8uU2VyaWFsaXphYmxlO67QCaxT1+1JAgAAeHAAAAACc3IADmphdmEubGFuZy5Mb25nO4vkkMyPI98CAAFKAAV2YWx1ZXhyABBqYXZhLmxhbmcuTnVtYmVyhqyVHQuU4IsCAAB4cAAAAAAAACcQdAAFVFRUVDI="
}

Just in case if someone is looking for converting any kind of object to Base64 string using something like Jackson or Gson. You could do something like this:
Convert your object to string using Jackson/Gson:
str = objectMapper.writeValueAsString(obj)
OR
str = gson.toJson(obj)
Now you get bytes from your string using
base64str = str.getBytes()
Use java's Base64 class to convert bytes to Base64 string:
Base64.getEncoder().encodeToString(base64str)

Related

Jackson serialize Object to JSON to base64 (without endless loop)

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;
}
}

Disable Base64 encoding while Serializing on Jackson ObjectMapper

I am using Jackson 2.2.3. When I serialize an object to JSON, all the byte[] properties are encoded to base64 in generated json.
How do I disable that?
Here is my code.
DataHolder.java
#XmlAccessorType(XmlAccessType.FIELD)
#XmlType(name = "", propOrder = {
"expectedData"
})
#XmlRootElement(name = "dataHolder")
public class DataHolder {
private final static long serialVersionUID = 1L;
#XmlElement(required = true, type = String.class)
#XmlJavaTypeAdapter(HexBinaryAdapter.class)
#XmlSchemaType(name = "hexBinary")
protected byte[] expectedData;
...
}
JSON output:
{
...
"dataHolder": {
"expectedData": "c29tZXRleHQ=",
}
...
}
Here I am creating custom Serializer :
public class BytesToStringSerializer extends StdSerializer<byte[]> {
public BytesToStringSerializer() {
super(byte[].class);
}
protected BytesToStringSerializer(Class<byte[]> t) {
super(t);
}
#Override
public void serialize(byte[] value, JsonGenerator gen, SerializerProvider provider) throws IOException {
String yourReadableString = new String(value, StandardCharsets.UTF_8);
gen.writeString(yourReadableString);
}
}
Your DataHolder class :
public class DataHolder {
#JsonSerialize(using = BytesToStringSerializer.class)
byte[] expectedData;
public DataHolder(byte[] expectedData) {
this.expectedData = expectedData;
}
}
And Main class for testing :
public class Main {
public static void main(String[] args) throws JsonProcessingException {
byte[] someBytes = "Hello world".getBytes(StandardCharsets.UTF_8);
DataHolder dataHolder = new DataHolder(someBytes);
ObjectMapper objectMapper = new ObjectMapper();
String output = objectMapper.writeValueAsString(dataHolder);
System.out.println(output);
}
}
The output is :
{"expectedData":"Hello world"}
Please keep in mind that you should use encoding thats is suitable for you. If your byte[] array does not represent anything readable you should keep it in base64 format.
EDIT :
To configure ObjectMapper globally register module with Serializer for it :
public class Main {
public static void main(String[] args) throws JsonProcessingException {
byte[] someBytes = "Hello world".getBytes(StandardCharsets.UTF_8);
DataHolder dataHolder = new DataHolder(someBytes);
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addSerializer(byte[].class, new BytesToStringSerializer());
objectMapper.registerModule(module);
String output = objectMapper.writeValueAsString(dataHolder);
System.out.println(output);
}
}
Remember to provide getters and setters for your data. If you dont want getters and setters configure objectMapper and set field visibility :
objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);

Custom BigInteger JSON serializer

I am trying to implement a custom JSON serializer class to display object BigInteger values as string in the JSON response.
I have implememented a custom serializer class
public class CustomCounterSerializer extends StdSerializer<BigInteger> {
private static final long serialVersionUID = 5440920327083673598L;
public CustomCounterSerializer() {
this(BigInteger.class);
}
public CustomCounterSerializer(Class<BigInteger> vc) {
super(vc);
}
#Override
public void serialize(BigInteger value, JsonGenerator jsonGenerator, SerializerProvider provider) throws IOException {
BigInteger valueJson = value==null ? BigInteger.valueOf(0) : value;
jsonGenerator.writeString(valueJson.toString());
}
}
The problem I have is that I want to handle the null object values using the overridden method and pass 0 to the JSON string and not null.
I have written a JUnit test for this:
public class CustomCounterSerializerTest {
private ObjectMapper objectMapper = new ObjectMapper();
#After
public void tearDown() {
objectMapper = null;
}
#Test
public void testCustomSerializerWithNullValues() throws JsonParseException, JsonMappingException, IOException {
MyObject obj = new MyObject();
obj.setNonNullValue(BigInteger.valueOf(1);
String obj_ = objectMapper.writeValueAsString(obj);
assertThat(obj_).isNotNull();
assertTrue(obj_.contains("\"nonNullValue\":\"" + BigInteger.valueOf(1).toString() + "\",\"nullValue\":\""+ BigInteger.valueOf(0).toString() +"\""));
}
}
It fails as it contains null and not nullValue:"0".
But the null value of the object always goes to no args constructor and even like this(BigInteger.class) does not use my method and prints null.
You need to tell Jackson that it should use your serializer
even for null values. You do this by using #JsonSerializer
also with the nullsUsing parameter.
In your MyObject class it would look like this:
#JsonSerialize(using = CustomCounterSerializer.class,
nullsUsing = CustomCounterSerializer.class)
private BigInteger nullValue;

ObjectMapper writeValueAsString changes bytebuffer pos

I am trying to serialize a complex object to string that somewhere contains bytebuffer inside by using ObjectMapper for logging the response.
This changes the cursor position inside the bytebuffer and simply corrupts the response.
Code snippet that i am using:
import org.codehaus.jackson.map.ObjectMapper;
private static final ObjectMapper MAPPER = new ObjectMapper();
public static String serializeToString(final Object obj) {
Preconditions.checkArgument(obj != null, "Object to be serialized is null");
try {
final String str = MAPPER.writeValueAsString(obj);
if (Strings.isNullOrEmpty(str)) {
log.warn("Serialized to null/empty string");
}
return str;
} catch (final JsonGenerationException e) {
throw new IllegalArgumentException("Json generation exception occured in de-serializing obj", e);
} catch (final JsonMappingException e) {
throw new IllegalArgumentException("Json mapping exception occured in de-serializing obj", e);
} catch (final IOException e) {
throw new IllegalArgumentException("IO exception occured in de-serializing obj", e);
}
}
I passed above method a complex object having bytebuffer inside.
I printed bytebuffer before and after calling above method.
public static void main(final String[] args) throws SecurityException, NoSuchMethodException {
final String x =
"Random data i am using for this test for byte buffer. Random data i am using for this test for byte buffer";
final byte[] byteArr = x.getBytes();
final ByteBuffer bb = ByteBuffer.wrap(byteArr);
System.out.println("before bytebuffer :" + bb);
String stringData = SerializerUtil.serializeToString(bb); // In real i am passing a complex structure having
// bytebuffer inside
System.out.println(stringData);
System.out.println("after bytebuffer :" + bb);
}
Output:
before bytebuffer :java.nio.HeapByteBuffer[pos=0 lim=106 cap=106]
{"short":21089,"char":"\u6e64","int":1869422692,"long":7022344510808023405,"float":2.0790493E-19,"double":6.687717052371733E223,"direct":false,"readOnly":false}
after bytebuffer :java.nio.HeapByteBuffer[pos=28 lim=106 cap=106]
This change in (pos=0 to pos=28)position simply corrupts the response sent. Do we have any way to convert this complex object to string without affecting the byteBuffer?
Any help much appreciated.
Obviously, you don't want to serialize the ByteBuffer property as another structured class, but just the content, as a string. One way to do that is to use a #JsonProperty annotation on a method to tell the mapper to use that method instead of trying to serialize the field directly. Assuming you have a bean like this:
class Stuff {
private ByteBuffer data;
public Stuff() {
}
public Stuff(ByteBuffer data) {
super();
this.data = data;
}
public ByteBuffer getData() {
return data;
}
public void setData(ByteBuffer data) {
this.data = data;
}
#JsonProperty(value = "data")
public String convertData() {
return new String(data.array());
}
#JsonProperty("data")
public void convertData(String s) {
data = ByteBuffer.wrap(s.getBytes());
}
}
The mapper will now use the convertData methods for serializeing and deserializing the ByteBuffer data property, and you can still use normal java bean property methods.
Update:
Since the serialized class cannot be changed, here is an alternative method using som advanced JACKSON stuff. First, create custom serializer and deserializer:
static class ByteBufferSerializer extends JsonSerializer<ByteBuffer> {
#Override
public void serialize(ByteBuffer value, JsonGenerator jgen,
SerializerProvider provider) throws IOException,
JsonProcessingException {
jgen.writeString(new String(value.array()));
}
}
static class ByteBufferDeserializer extends JsonDeserializer<ByteBuffer> {
#Override
public ByteBuffer deserialize(JsonParser jp,
DeserializationContext context) throws IOException,
JsonProcessingException {
return ByteBuffer.wrap(jp.getText().getBytes());
}
}
Then, create a Mixin interface to provide the annotations for the properties that we cannot provide in the real target class:
static interface Mixin {
#JsonSerialize(using = ByteBufferSerializer.class, contentAs = String.class)
ByteBuffer getData();
#JsonDeserialize(using = ByteBufferDeserializer.class, contentAs = String.class)
void setData(ByteBuffer data);
}
Further, create a Module used to configure the object mapper, and add the mixin interface:
static class MyModule extends SimpleModule {
public MyModule() {
super("ByteBuffer wrangling");
}
#Override
public void setupModule(SetupContext context) {
context.setMixInAnnotations(Stuff.class, Mixin.class);
}
}
And, finally, register the module with the mapper:
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new MyModule());
Voilà, piece of cake! :-)

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