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
Httpclient 4.0.1 - How to Release Connection
Retrieve Java Annotation Attribute
Check If File Exists on Remote Server Using Its Url
How to Create a Process in Java
Java.Net.Connectexception :Connection Timed Out: Connect
Check If Int Is Between Two Numbers
Java: How to Test on Array Equality
Intellij - Convert a Java Project/Module into a Maven Project/Module
How to Capture Https with Fiddler, in Java
Splitting a CSV File with Quotes as Text-Delimiter Using String.Split()
Spring Cron Expression for Every Day 1:01:Am
Java String.Indexof and Empty Strings
Tomcat in Idea. War Exploded: Server Is Not Connected. Deploy Is Not Available
Jce Cannot Authenticate the Provider Bc in Java Swing Application
How to Pause/Sleep/Wait in a Java Swing App
Java JSON Serialization - Best Practice