Jackson: How to Add Custom Property to the JSON Without Modifying the Pojo

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

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

Jackson: How to edit existing property to the JSON without modifying the POJO?

Mixins

The easiest way to modify the output of Jackson without adding annotations to the original POJO is using mixins.

Just define a mixin-class with the necessary annotations and indicate to Jackson that you want to use the mixin when serializing the original object.

private static class MyPojoMixin {
@JsonProperty("cust_id")
private String id;
}

public String serializeWithMixin(MyPojo p) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
mapper.addMixIn(MyPojo.class, MyPojoMixin.class);

return mapper.writeValueAsString(p);
}

Custom property naming strategy

If you need to programmatically change the field-name, you might not be able to use the mixin solution. You could then use a custom PropertyNamingStrategy:

public class IdRenamingStrategy extends PropertyNamingStrategy {
private final PropertyNamingStrategy inner;
private final String newIdPropertyName;

public IdRenamingStrategy(String newIdPropertyName) {
this(PropertyNamingStrategy.LOWER_CAMEL_CASE, newIdPropertyName);
}

public IdRenamingStrategy(PropertyNamingStrategy inner, String newIdPropertyName) {
this.inner = inner;
this.newIdPropertyName = newIdPropertyName;
}

private String translate(String propertyName) {
if ("id".equals(propertyName)) {
return newIdPropertyName;
} else {
return propertyName;
}
}

@Override
public String nameForField(MapperConfig<?> config, AnnotatedField field, String defaultName) {
return inner.nameForField(config, field, translate(defaultName));
}

@Override
public String nameForGetterMethod(MapperConfig<?> config, AnnotatedMethod method, String defaultName) {
return inner.nameForGetterMethod(config, method, translate(defaultName));
}

@Override
public String nameForSetterMethod(MapperConfig<?> config, AnnotatedMethod method, String defaultName) {
return inner.nameForSetterMethod(config, method, translate(defaultName));
}

@Override
public String nameForConstructorParameter(MapperConfig<?> config, AnnotatedParameter ctorParam, String defaultName) {
return inner.nameForConstructorParameter(config, ctorParam, translate(defaultName));
}
}

This can be used like this:

public String serializeWithPropertyNamingStrategy(MyPojo p) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
mapper.setPropertyNamingStrategy(new IdRenamingStrategy("cust_id"));

return mapper.writeValueAsString(p));
}

Jackson How to add additional properties during searlization without making changes to default POJO?

You can try like below in the controller level, (it would be better to manipulate the response by using the Interceptor or Filter)

   @GetMapping
public ObjectNode test() throws JsonProcessingException {

CustomClass customClass = new CustomClass();
customClass.setAge(30);
customClass.setName("Batman");

ObjectMapper mapper = new ObjectMapper();
String jsonStr = mapper.writeValueAsString(customClass);
ObjectNode nodes = mapper.readValue(jsonStr, ObjectNode.class);
nodes.put("job", "HR ");
nodes.put("company", "New ");

return nodes;
}

Response:

{
name: "Batman",
age: 30,
job: "HR ",
company: "New "
}

Your new test driver is below,

 public static void main(String[] args) throws JsonProcessingException {
CustomClass customClass = new CustomClass();
customClass.setAge(30);
customClass.setName("Batman");

ObjectMapper mapper = new ObjectMapper();
String jsonStr = mapper.writeValueAsString(customClass);
ObjectNode nodes = mapper.readValue(jsonStr, ObjectNode.class);
nodes.put("job", "HR ");
nodes.put("company", "New ");
System.out.println(nodes);

}

Output: {"name":"Batman","age":30,"job":"HR ","company":"New "}


Updated

but I get the error: Type id handling not implemented for type
package.ClassName (by serializer of type
package.CustomModule$CustomClassSerializer)

Write new object fields "job" and "company" to your custom class serializer.

public class Test {
public static void main(String args[]) throws Exception {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new CustomModule());
CustomClass cc = new CustomClass();
cc.setAge(30);
cc.setName("Batman");
StringWriter sw = new StringWriter();
objectMapper.writeValue(sw, cc);
System.out.println(sw.toString());
}

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("job", "HR ");
jgen.writeObjectField("company", "New ");
jgen.writeEndObject();
}
}
}
}

Output: {"name":"Batman","age":30,"job":"HR ","company":"New "}

Jackson: Add dynamic field to Map serialization (sth like @JsonAppend)

Great question! Yes, this is (somehow) possible. The following exposed methodology maintains the standard serialization behavior, while adding on top of it annotation-defined key-value pairs.


Create a custom annotation. I'll call it MapAppender

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MapAppender {
String[] keys();
String[] values();
}

As you can see, we define key-value arrays, which will match by index.

We are forced using String fields instead of the more generic Object, but that's per annotation design.

Create a custom JsonSerializer<Map>. I'll call it MapAppenderSerializer

public class MapAppenderSerializer
extends StdSerializer<Map>
implements ContextualSerializer {
private static final long serialVersionUID = 1L;

private final String[] keys;
private final String[] values;

// No-arg constructor required for Jackson
MapAppenderSerializer() {
super(Map.class);
keys = new String[0];
values = new String[0];
}

MapAppenderSerializer(
final String[] keys,
final String[] values) {
super(Map.class);
this.keys = keys;
this.values = values;
}

@Override
public void serialize(
final Map value,
final JsonGenerator jsonGenerator,
final SerializerProvider serializerProvider) throws IOException {
// Create a copy Map to avoid touching the original one
final Map hashMap = new HashMap<>(value);

// Add the annotation-specified key-value pairs
for (int i = 0; i < keys.length; i++) {
hashMap.put(keys[i], values[i]);
}

// Serialize the new Map
serializerProvider.defaultSerializeValue(hashMap, jsonGenerator);
}

@Override
public JsonSerializer<?> createContextual(
final SerializerProvider serializerProvider,
final BeanProperty property) {
MapAppender annotation = null;

if (property != null) {
annotation = property.getAnnotation(MapAppender.class);
}

if (annotation != null) {
return new MapAppenderSerializer(annotation.keys(), annotation.values());
}

throw new UnsupportedOperationException("...");
}
}

Now, using your Bean class example, annotate the Map field with @MapAppender and define a custom serializer using @JsonSerialize

public class Bean {
public String simpleField;

@MapAppender(keys = {"test1", "test2"}, values = {"value1", "value2"})
@JsonSerialize(using = MapAppenderSerializer.class)
public Map<Object, Object> simpleMap = new HashMap<>();
}

That's it. Serializing an instance of Bean

final ObjectMapper objectMapper = new ObjectMapper();
final String string = objectMapper.writeValueAsString(new Bean());

results in

{"simpleField":null,"simpleMap":{"test2":"value2","test1":"value1"}}

Another example, having the Map populated with values prior to serialization

final ObjectMapper objectMapper = new ObjectMapper();
final Bean value = new Bean();
value.simpleMap.put("myKey", "myValue");

final String string = objectMapper.writeValueAsString(value);

results in

{"simpleField":null,"simpleMap":{"test1":"value1","test2":"value2","myKey":"myValue"}}

Jackson add custom field with hash of another field

This is quite interesting. I think you can solve this with BeanSerializerModifier.

The idea is to register a custom serialiser which would have access to the original bean serialiser, the property description and the object value. If the property is annotated with a GenerateHash annotation, then the serialiser will emit an additional field. Here is an example:

public class JacksonGenerateHash {
@Retention(RetentionPolicy.RUNTIME)
public static @interface GenerateHash {
}

public static class Bean {
@GenerateHash
public final String value;

public Bean(final String value) {
this.value = value;
}
}

private static class MyBeanSerializerModifier extends BeanSerializerModifier {
@Override
public JsonSerializer<?> modifySerializer(
final SerializationConfig serializationConfig,
final BeanDescription beanDescription,
final JsonSerializer<?> jsonSerializer) {
return new HashGeneratingSerializer((JsonSerializer<Object>) jsonSerializer, null);
}
}

private static class HashGeneratingSerializer extends JsonSerializer<Object>
implements ContextualSerializer {
private final JsonSerializer<Object> serializer;
private final BeanProperty property;

public HashGeneratingSerializer(
final JsonSerializer<Object> jsonSerializer,
final BeanProperty property) {
this.serializer = jsonSerializer;
this.property = property;
}

@Override
public void serialize(
final Object o,
final JsonGenerator jsonGenerator,
final SerializerProvider serializerProvider)
throws IOException {
serializer.serialize(o, jsonGenerator, serializerProvider);
// if the generatehash is present the property must be set
if (property != null) {
jsonGenerator.writeNumberField("_hash_" + property.getName(), o.hashCode());
}
}
// override this method to access the bean property
@Override
public JsonSerializer<?> createContextual(
final SerializerProvider prov, final BeanProperty property)
throws JsonMappingException {
if (property != null && property.getAnnotation(GenerateHash.class) != null) {
return new HashGeneratingSerializer(serializer, property);
}
return serializer;
}
}

public static void main(String[] args) throws JsonProcessingException {
SimpleModule module = new SimpleModule();
module.setSerializerModifier(new MyBeanSerializerModifier());
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);
System.out.println(mapper.writeValueAsString(new Bean("abc")));
}
}

Output:

{"value":"abc","_hash_value":96354}

Ignoring a field without modifying the POJO class with Jackson

Configuring Jackson to use only field annotations

Once the annotations are placed on the fields, you can configure ObjectMapper to use only the fields annotations, ignoring the annotations of getters and setters methods:

ObjectMapper mapper = new ObjectMapper();    
mapper.setVisibility(PropertyAccessor.ALL, Visibility.NONE);
mapper.setVisibility(PropertyAccessor.FIELD, Visibility.ANY);

Jackson mix-in annotations

It's a great alternative when modifying your POJOs is not an option. You can think of it as a kind of aspect-oriented way of adding more annotations during runtime, to augment statically defined ones.

Define a mix-in annotation interface (class would do as well):

public interface FooMixIn {

@JsonIgnore
String getBar();
}

Then configure ObjectMapper to use the defined interface (or class) as a mix-in for your POJO:

ObjectMapper mapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT)
.addMixIn(Foo.class, FooMixIn.class);

Here are some usage considerations:

  • All annotation sets that Jackson recognizes can be mixed in.
  • All kinds of annotations (member method, static method, field, constructor annotations) can be mixed in.
  • Only method (and field) name and signature are used for matching annotations: access definitions (private, protected, ...) and method implementations are ignored.

For more details, check this page.

Jackson serialise property based on other property value

Jackson has following concepts for that:

Filter: filter and control serialization of properties

How to setup:
The annotation @JsonFilter applies a named filter to a class (bean).
That name can be looked up in a registry to find a custom PropertyFilter implementation (e.g. an extension of abstract SimpleBeanPropertyFilter).
The registry is a FilterProvider which can be configured with your ObjectMapper.

How it works:
So when the ObjectMapper instance is about to serialize your bean it will recognize the filter and use the logic to a FilterProvider. The FilterProvider then controls if and how the property is serialized.

See:

  • tutorial on Baeldung (2019): Serialize Only Fields that meet a Custom Criteria with Jackson
  • article on CowTownCoder (2011): Advanced filtering with Jackson, Json Filters

Modifier: modify serialization at runtime, dynamically/conditional

For example BeanSerializerModifier as solution to hide a field dynamically - based on a runtime value condition. See Skip objects conditionally when serializing with jackson.

Static masking

(Probably not for your variable key/value pairs, but generally good approach for fields designed to hide sensitive data)

See Mask json fields using jackson which tries to add a specific serializer which derives its mask from a custom annotation.

Adding property inside property in jackson

You must use the ObjectNode corresponding to the object to which you want to add your property. Try, for example: bufferFeature.with("properties").put("prop1", "value").

See the answer to this question.

Jackson JSON Serialization without field name

From the wiki page it sounds like the @JsonUnwrapped annotation should do what you want.

@JsonUnwrapped: property annotation used to define that value should be "unwrapped" when serialized (and wrapped again when deserializing), resulting in flattening of data structure, compared to POJO structure.

The Javadoc for the class also has an example that looks appropriate.



Related Topics



Leave a reply



Submit