Configure Jackson to honor SerializationInclusion configuration for custom Serializer classes - java

I'm having a rather interesting problem attempting to get Jackson to properly remove null fields from resulting JSON when they have been created by a custom serializer class. I've searched pretty thoroughly for information regarding Serializer and the SerializationInclusion configuration, but I haven't found anything that seems to explain what I'm seeing.
I have a Jackson object mapper configured and autowired in via Spring. The object mapper configuration and POJOs (edited for brevity) look more or less like the code below.
For some reason, when I call our REST endpoint to get a Bar object back that contains the above example values I see the following behavior:
The SerializationInclusion setting is being applied to all properties that are empty or null on their own (name, aList, objId).
The SerializationInclusion setting is NOT being applied to any properties that are set to null by a custom Serializer class, and we get back JSON that has a null value present.
My thought here is that the Serializer logic gets called AFTER the Jackson has already removed all null and empty values from the JSON, and therefore the "foo" property is properly set to null, but is not removed because the inclusion logic has already executed.
Does anyone have any thoughts about what might be going on here? Is this a quirk in how Jackson-databind library is implemented in version 2.2.2?
Jackson Config -
#Bean
public JacksonObjectMapper jacksonMapper() {
final JacksonObjectMapper mapper = new JacksonObjectMapper();
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY);
mapper.enable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS);
mapper.registerModule(agJacksonModule());
return mapper;
}
#Bean
public SimpleModule agJacksonModule() {
final SimpleModule module = new SimpleModule();
module.addSerializer(Foo.class, new FooSerializer());
return module;
}
Custom Serializer -
public class FooSerializer extends JsonSerializer<Foo> {
#Override
public void serialize(Sponsor sponsor, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
throws IOException {
// write null value for sponsor json property, if sponsor object has all empty or null fields
if(sponsor == null || isObjectEmpty(sponsor)) {
jsonGenerator.writeNull();
return;
}
// write out object
jsonGenerator.writeStartObject();
jsonGenerator.writeStringField("imgUrl", sponsor.getImgUrl());
jsonGenerator.writeStringField("clickUrl", sponsor.getClickUrl());
jsonGenerator.writeStringField("sponsorName", sponsor.getSponsorName());
jsonGenerator.writeStringField("sponsorText", sponsor.getSponsorText());
jsonGenerator.writeEndObject();
}
private boolean isObjectEmpty(Sponsor sponsor) {
return Strings.isNullOrEmpty(sponsor.getClickUrl())
&& Strings.isNullOrEmpty(sponsor.getImgUrl())
&& Strings.isNullOrEmpty(sponsor.getSponsorName())
&& Strings.isNullOrEmpty(sponsor.getSponsorText());
}
}
The object model looks something like this (again edited for brevity, sample values are set on the class members as example data):
Bar POJO -
public abstract class Bar {
protected Foo foo = aFoo;
protected String name = "";
protected ArrayList aList = Lists.newArrayList();
protected String objId = null;
// some getters and setters for the above properties
}
Foo POJO -
public abstract class Foo {
protected String aString = "";
protected String bString = "";
protected String cString = "";
protected String dString = "";
// some getters and setters for the above properties
}

Override and implement the isEmpty method of JsonSerializer to achieve what you want.
For custom definition of what emptymeans, your JsonSerializer implementation needs to override this method:
public boolean isEmpty(SerializerProvider provider, T value);
Note that it is the caller that has to handle filtering as it writes field name; serializer will only be called in case actual serialization is needed.

Related

Custom Jackson serializer on specific fields

I'm looking to have multiple jackson deserializers for the same object(s) all based on a custom annotation.
Ideally I'd have a single POJO like:
public class UserInfo {
#Redacted
String ssn;
String name;
}
Under "normal" conditions I want this object to be serialized the default way:
{"ssn":"123-45-6789", "name":"Bob Smith"}
but for logging purposes (for example) I want to redact the SSN so it doesn't get saved in our logs:
{"ssn":"xxx-xx-xxxx", "name":"Bob Smith"}
I've also looked into using #JsonSerialize and come up with:
public class UserInfo {
#JsonSerialize(using = RedactedSerializer.class, as=String.class)
String firstName;
String lastName;
}
The problem with this is that it ALWAYS uses this rule. Can multiple #JsonSerializers be added and only the specified one be used within the runtime code?
I've also seen "views" but ideally I'd like to atleast show that the field was present on the request - even if I dont know the value.
The 100% safe way would be to use different DTO in different requests. But yeah, if you cant do that, use #JsonView and custom serializer, something like:
class Views {
public static class ShowSSN {}
}
private static class MyBean{
#JsonSerialize(using = MyBeanSerializer.class)
#JsonView(Views.ShowSSN.class)
String ssn;
//getter setter constructor
}
private class MyBeanSerializer extends JsonSerializer<String> {
#Override
public void serialize(String value, JsonGenerator gen,
SerializerProvider serializers) throws IOException {
Class<?> jsonView = serializers.getActiveView();
if (jsonView == Views.ShowSSN.class)
gen.writeString(value); // your custom serialization code here
else
gen.writeString("xxx-xx-xxxx");
}
}
And use it like:
public static void main(String[] args) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
MyBean bean = new MyBean("123-45-6789");
System.out.println(mapper.writerWithView(Views.ShowSSN.class)
.writeValueAsString(bean));
// results in {"ssn":"123-45-6789"}
System.out.println(mapper.writeValueAsString(bean));
// results in {"ssn":"xxx-xx-xxxx"}
}
Also for example in spring it would be really easy to use
#Controller
public class MyController {
#GetMapping("/withView") // results in {"ssn":"123-45-6789"}
#JsonView(Views.ShowSSN.class)
public #ResponseBody MyBean withJsonView() {
return new MyBean("123-45-6789");
}
#GetMapping("/withoutView") // results in {"ssn":"xxx-xx-xxxx"}
public #ResponseBody MyBean withoutJsonView() {
return new MyBean("123-45-6789");
}
}
I think you could achieve that dynamically by coding not annotations,
inside your methods, you can set the proper Serializer and switch between them
(The code depends on your Jackson version)
ObjectMapper mapper = new ObjectMapper();
SimpleModule testModule = new SimpleModule("MyModule", new Version(1, 0, 0, null));
testModule.addSerializer(new RedactedSerializer()); // assuming serializer declares correct class to bind to
mapper.registerModule(testModule);
https://github.com/FasterXML/jackson-docs/wiki/JacksonHowToCustomSerializers

Jackson custom type info not working

I've got classes which use an #JsonTypeIdResolver to add a custom type field to the output. This code was working as expected. I've now added an PropertyFilter to my mapper object. This is where the #JsonTypeIdResolver stopped working. The factory is not being called anymore.
Working code:
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(outputStream,myObject);
Not working code:
ObjectMapper mapper = new ObjectMapper();
PropertyFilter myfilter=new SimpleBeanFilter() {
protected boolean include(BeanPropertyWriter writer) {
return true;
}
protected boolean include(PropertyWriter writer) {
return true;
}
}
FilterProvider filters=new SimpleFilterProvider().addFilter("myFilter",myFilter);
mapper.writer(filter).writeValue(outputStream,myObject);
As the filter is just useless (accepts anything) the output should be the same. Why does my type field not get serialized anymore?
Seems like Jackson doesn't deal with inheritance the right way. My test setup was like
#JsonTypeInfo( use = JsonTypeInfo.Id.CLASS, include = As.PROPERTY, property = "_type" )
abstract class Base {
String somefield;
...
}
class ChildA extends Base {
...
}
class ChildB extends Base {
...
}
If I write a custom serializer, which explicitely casts ChildA and ChildB to Base before serializing, it works as expected. So the basic issue is that jackson does not recognize annotations on parent objects, if not explicitly told to do so.

How to implement Jackson custom serialization outside a domain bean?

I have a Spring managed bean...
#Component("Foobean")
#Scope("prototype")
public class foobean {
private String bar1;
private String bar2;
public String getBar1() {
return bar1;
}
public void setBar1(String bar1) {
this.bar1 = bar1;
}
public String getBar2() {
return bar2;
}
public void setBar2(String bar2) {
this.bar2 = bar2;
}
}
...and because I am using Dojo Dgrid to display an ArrayList of this bean, I am returning it into the controller as a JSON string:
#Controller
#RequestMapping("/bo")
public class FooController {
#Autowired
private FooService fooService
#RequestMapping("action=getListOfFoos*")
#ResponseBody
public String clickDisplayFoos(
Map<String, Object> model) {
List<Foobean> foobeans = fooService.getFoobeans();
ObjectMapper objMapper = new ObjectMapper();
String FooJson = null;
try {
FooJson = objMapper.writeValueAsString(foobeans);
} catch (JsonGenerationException e) {
etc.
}
However, my grid needs an additional column which will contain a valid action for each Foo; that action is not really dependent on any data in individual Foos -- they'll all have the same valid action -- repeated on each line of the resulting DGrid -- but that value is actually dependent upon security roles on the session...which can't be sent to the front end in a Json. So, my solution is twofold:
First I need to add a "virtual" Json property to the bean... which I can do in the bean with #JsonProperty on a method...
#JsonProperty("validActions")
public String writeValidActions {
return "placeHolderForSerializerToChange";
}
...but it just generates a placeholder. To really generate a valid value,
I need to reference the security role of the session,
which I am very reluctant to code in the above method. (A service call in
the domain bean itself? Seems very wrong.) I
think I should create a custom serializer and put the logic -- and the reference
to the Session.Security role in there. Are my instincts right, not to
inject session info into a domain bean method? And if so, what would such a
custom serializer look like?
Yes, I wouldn't put Session Info in to the domain or access session directly in my domain.
Unless there is a specific reason, you could simply add the logic in your action class.
public String clickDisplayFoos(){
List<Foo> foos = service.getFoos();
for(iterate through foos){
foo.setValidAction(session.hasSecurityRole())
}
String json = objMapper.writeValueAsString(foobeans);
return json;
}
I don't like the idea of setting new values as part of the serialization process. I feel custom serializers are meant to transform the representation of a particular property rather than add new values to a property.

How to (De)serialize field from object based on annotation using Jackson?

I need to configure Jackson in a specific way which I'll describe below.
Requirements
Annotated fields are serialized with only their id:
If the field is a normal object, serialize its id
If the field is a collection of objects, serialize an array of id
Annotated fields get their property names serialized differently:
If the field is a normal object, add "_id" suffix to property name
If the field is a collection of objects, add "_ids" suffix to property name
For the annotation I was thinking something like a custom #JsonId, ideally with an optional value to override the name just like #JsonProperty does
The id property should be defined by the user, either using:
The already existing Jackson's #JsonIdentityInfo
Or by creating another class or field annotation
Or by deciding which annotation to inspect for id property discoverability (useful for JPA scenarios, for example)
Objects should be serialized with a wrapped root value
Camel case naming should be converted to lower case with underscores
All of this should be deserializable (by constructing an instance with just the id setted)
An example
Considering these POJO's:
//Inform Jackson which property is the id
#JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id"
)
public abstract class BaseResource{
protected Long id;
//getters and setters
}
public class Resource extends BaseResource{
private String name;
#JsonId
private SubResource subResource;
#JsonId
private List<SubResource> subResources;
//getters and setters
}
public class SubResource extends BaseResource{
private String value;
//getters and setters
}
A possible serialization of a Resource instance could be:
{
"resource":{
"id": 1,
"name": "bla",
"sub_resource_id": 2,
"sub_resource_ids": [
1,
2,
3
]
}
}
So far...
Requirement #5 can be accomplished by configuring ObjectMapper in the following way:
objectMapper.configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true);
objectMapper.configure(SerializationFeature.WRAP_ROOT_VALUE, true);
And then using #JsonRootName("example_root_name_here") in my POJO's.
Requirement #6 can be accomplished by configuring ObjectMapper in the following way:
objectMapper.setPropertyNamingStrategy(
PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);
As you can see there are still lots of requirements to fulfill. For those wondering why I need such a configuration, it's because I'm developing a REST webservice for ember.js (more specifically Ember Data).
You would appreciate very much if you could help with any of the requirements.
Thanks!
Most (all?) of your requirements can be accomplished through the use of a contextual serializer. Taking one answer from ContextualDeserializer for mapping JSON to different types of maps with Jackson and Jackson's wiki (http://wiki.fasterxml.com/JacksonFeatureContextualHandlers) I was able to come up with the following.
You need to start with the #JsonId annotation, which is the key indicating a property needs to only use the Id property.
import com.fasterxml.jackson.annotation.*;
import java.lang.annotation.*;
#Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
#JacksonAnnotation // important so that it will get included!
public #interface JsonId {
}
Next is the actual ContextualSerializer, which does the heavy lifting.
import com.fasterxml.jackson.databind.ser.*;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.core.*;
import java.io.*;
public class ContextualJsonIdSerializer
extends JsonSerializer<BaseResource>
implements ContextualSerializer/*<BaseResource>*/
{
private ObjectMapper mapper;
private boolean useJsonId;
public ContextualJsonIdSerializer(ObjectMapper mapper) { this(mapper, false); }
public ContextualJsonIdSerializer(ObjectMapper mapper, boolean useJsonId) {
this.mapper = mapper;
this.useJsonId = useJsonId;
}
#Override
public void serialize(BaseResource br, JsonGenerator jgen, SerializerProvider provider) throws IOException
{
if ( useJsonId ) {
jgen.writeString(br.getId().toString());
} else {
mapper.writeValue(jgen, br);
}
}
#Override
public JsonSerializer<BaseResource> createContextual(SerializerProvider config, BeanProperty property)
throws JsonMappingException
{
// First find annotation used for getter or field:
System.out.println("Finding annotations for "+property);
if ( null == property ) {
return new ContextualJsonIdSerializer(mapper, false);
}
JsonId ann = property.getAnnotation(JsonId.class);
if (ann == null) { // but if missing, default one from class
ann = property.getContextAnnotation(JsonId.class);
}
if (ann == null ) {//|| ann.length() == 0) {
return this;//new ContextualJsonIdSerializer(false);
}
return new ContextualJsonIdSerializer(mapper, true);
}
}
This class looks at BaseResource properties and inspects them to see if the #JsonId annotation is present. If it is then only the Id property is used, otherwise a passed in ObjectMapper is used to serialize the value. This is important because if you try to use the mapper that is (basically) in the context of the ContextualSerializer then you will get a stack overflow since it will eventually call these methods over and over.
You're resource should look something like the following. I used the #JsonProperty annotation instead of wrapping the functionality in the ContextualSerializer because it seemed silly to reinvent the wheel.
import java.util.*;
import com.fasterxml.jackson.annotation.*;
public class Resource extends BaseResource{
private String name;
#JsonProperty("sub_resource_id")
#JsonId
private SubResource subResource;
#JsonProperty("sub_resource_ids")
#JsonId
private List<SubResource> subResources;
//getters and setters
public String getName() {return name;}
public void setName(String name) {this.name = name;}
public SubResource getSubResource() {return subResource;}
public void setSubResource(SubResource subResource) {this.subResource = subResource;}
public List<SubResource> getSubResources() {return subResources;}
public void setSubResources(List<SubResource> subResources) {this.subResources = subResources;}
}
Finally the method that performs the serialization just creates an additional ObjectMapper and registers a module in the original ObjectMapper.
// Create the original ObjectMapper
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true);
objectMapper.configure(SerializationFeature.WRAP_ROOT_VALUE, true);
objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);
// Create a clone of the original ObjectMapper
ObjectMapper objectMapper2 = new ObjectMapper();
objectMapper2.configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true);
objectMapper2.configure(SerializationFeature.WRAP_ROOT_VALUE, true);
objectMapper2.setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);
// Create a module that references the Contextual Serializer
SimpleModule module = new SimpleModule("JsonId", new Version(1, 0, 0, null));
// All references to SubResource should be run through this serializer
module.addSerializer(SubResource.class, new ContextualJsonIdSerializer(objectMapper2));
objectMapper.registerModule(module);
// Now just use the original objectMapper to serialize

Create simple JSON structure using jackson

I'd just like to create the Jackson mapping equivalent of the below :
{\"isDone\": true}
I think I need to create a class like this :
public class Status {
private boolean isDone;
public boolean isDone{
return this.isDone;
}
public void setDone(boolean isDone){
this.isDone = isDone;
}
}
But how do I instatiate it and then write the JSON to a string ?
A problem with your example and Jackson is the default choices of JSON property names: Jackson will see isDone and setDone and choose done as the JSON property name. You can override this default choice using the JsonProperty annotation:
public class Status
{
private boolean isDone;
#JsonProperty("isDone")
public boolean isDone()
{
return this.isDone;
}
#JsonProperty("isDone")
public void setDone(boolean isDone)
{
this.isDone = isDone;
}
}
Then:
Status instance = new Status();
String jsonString = null;
instance.setDone(true);
ObjectMapper mapper = new ObjectMapper();
jsonString = mapper.writeValueAsString(instance);
Now jsonString contains { "isDone" : true }. Note that you can also write the string to an OutputStream using ObjectMapper.writeValue(OutputStream, Object), or to a Writer using ObjectMapper.writeValue(Writer, Object).
In this case you really only need the JsonProperty annotation on either of your accessors, but not both. Just annotating isDone will get you the JSON property name that you want.
An alternative to using the JsonProperty annotation is to rename your accessors setIsDone/getIsDone. Then the annotations are unnecessary.
See the quick and dirty Jackson tutorial: Jackson in 5 minutes. Understanding of the specific properties came from looking through the docs for Jackson annotations.
Right. The code needed:
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.writeValueAsString(new Status()));

Categories