I want to have several different schema when serializing with Jackson.
Suppose that I have the following classes:
public class Department {
private Person head;
private Person deputy;
private List<Person> staff;
// getters and setters
}
public class Person {
private String name
private int code;
// getters and setters
}
Now, I want to have two different schema for Department class. The first one contains only head and deputy where head includes both name and code, but deputy has only name. The second schema should include all fields recursively.
Thus, we will have two different jsons. With the first schema:
{
"head" : {
"name" : "John",
"code" : 123
},
"deputy" : {
"name" : "Jack"
}
}
, and with the second schema:
{
"head" : {
"name" : "John",
"code" : 123
},
"deputy" : {
"name" : "Jack",
"code" : "234"
},
"staff": [
{
"name" : "Tom",
"code" : "345"
},
{
"name" : "Matt",
"code" : "456"
},
]
}
QUESTION: How should I do it with Jackson?
NOTE: These classes are just examples. For this simple example, writing four different wrapper classes may be possible but think about a complex example with dozen of classes that each one has several fields. Using wrapper classes, we should generate a lot of boilerplate code.
Any help would be appreciated!
Although the #bigbounty solution is excellent and I think that Views, in addition to specific DTOs, are the way to go in general, in this case it may not be applicable because, for the same class Person we actually need two different behaviors in the same view.
#JsonFilters can be used to solve the problem.
This main method computes the graph what you need:
public static void main(String[] args) throws JsonProcessingException {
final PropertyFilter departmentFilter = new SimpleBeanPropertyFilter() {
#Override
public void serializeAsField
(Object pojo, JsonGenerator jgen, SerializerProvider provider, PropertyWriter writer)
throws Exception {
if (include(writer)) {
final String name = writer.getName();
if (!name.equals("deputy") && !name.equals("staff")) {
writer.serializeAsField(pojo, jgen, provider);
return;
}
if (name.equals("staff")) {
return;
}
// Ideally it should not be muted.
final Department department = (Department)pojo;
final Person deputy = department.getDeputy();
deputy.setCode(-1);
writer.serializeAsField(department, jgen, provider);
} else if (!jgen.canOmitFields()) { // since 2.3
writer.serializeAsOmittedField(pojo, jgen, provider);
}
}
#Override
protected boolean include(BeanPropertyWriter writer) {
return true;
}
#Override
protected boolean include(PropertyWriter writer) {
return true;
}
};
final PropertyFilter personFilter = new SimpleBeanPropertyFilter() {
#Override
public void serializeAsField
(Object pojo, JsonGenerator jgen, SerializerProvider provider, PropertyWriter writer)
throws Exception {
if (include(writer)) {
if (!writer.getName().equals("code")) {
writer.serializeAsField(pojo, jgen, provider);
return;
}
int code = ((Person) pojo).getCode();
if (code >= 0) {
writer.serializeAsField(pojo, jgen, provider);
}
} else if (!jgen.canOmitFields()) { // since 2.3
writer.serializeAsOmittedField(pojo, jgen, provider);
}
}
#Override
protected boolean include(BeanPropertyWriter writer) {
return true;
}
#Override
protected boolean include(PropertyWriter writer) {
return true;
}
};
final Department department = new Department();
final Person head = new Person("John", 123);
final Person deputy = new Person("Jack", 234);
final List<Person> personList = Arrays.asList(new Person("Tom", 345), new Person("Matt", 456));
department.setHead(head);
department.setDeputy(deputy);
department.setStaff(personList);
final ObjectMapper mapper = new ObjectMapper();
final FilterProvider schema1Filters = new SimpleFilterProvider()
.addFilter("deparmentFilter", departmentFilter)
.addFilter("personFilter", personFilter)
;
mapper.setFilterProvider(schema1Filters);
final String withSchema1Filters = mapper.writeValueAsString(department);
System.out.printf("Schema 1:\n%s\n", withSchema1Filters);
// You must maintain the filters once the classes are annotated with #JsonFilter
// We can use two no-op builtin filters
final FilterProvider schema2Filters = new SimpleFilterProvider()
.addFilter("deparmentFilter", SimpleBeanPropertyFilter.serializeAll())
.addFilter("personFilter", SimpleBeanPropertyFilter.serializeAll())
;
mapper.setFilterProvider(schema2Filters);
final String withSchema2Filters = mapper.writeValueAsString(department);
System.out.printf("Schema 2:\n%s\n", withSchema2Filters);
}
For this code to work, you must annotate the Department class with:
#JsonFilter("deparmentFilter")
And the Person class with:
#JsonFilter("personFilter")
As you can see, Jackson also provides several builtin filters.
This code is very coupled to the test classes that you proposed but it can be extended in ways that makes it more generic.
Please, take a look at SimpleBeanPropertyFilter for examples of how to create your own filter.
There is a feature JsonViews in the Jackson library.
You need to have a class for the views
public class Views {
public static class Normal{}
public static class Extended extends Normal{}
}
Next you annotate the Department class
import com.fasterxml.jackson.annotation.JsonView;
import java.util.List;
public class Department {
#JsonView(Views.Normal.class)
private Person head;
#JsonView(Views.Normal.class)
private Person deputy;
#JsonView(Views.Extended.class)
private List<Person> staff;
public Person getHead() {
return head;
}
public void setHead(Person head) {
this.head = head;
}
public Person getDeputy() {
return deputy;
}
public void setDeputy(Person deputy) {
this.deputy = deputy;
}
public List<Person> getStaff() {
return staff;
}
public void setStaff(List<Person> staff) {
this.staff = staff;
}
}
Keep the Person class as it is
public class Person {
private String name;
private int code;
public Person(String name, int code) {
this.name = name;
this.code = code;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
}
In the main function, where you are serializing the object, you need to enable the corresponding views.
public class Main {
static Department createDepartment(){
Department department = new Department();
Person head = new Person("John", 123);
Person deputy = new Person("Jack", 234);
List<Person> personList = Arrays.asList(new Person("Tom", 345), new Person("Matt", 456));
department.setHead(head);
department.setDeputy(deputy);
department.setStaff(personList);
return department;
}
public static void main(String[] args) throws JsonProcessingException {
Department department = createDepartment();
ObjectMapper mapper = new ObjectMapper();
String normal = mapper.writerWithView(Views.Normal.class).writeValueAsString(department);
String extended = mapper.writerWithView(Views.Extended.class).writeValueAsString(department);
System.out.println("Normal View - " + normal);
System.out.println("Extended View - " + extended);
}
}
The output is as follows:
Normal View - {"head":{"name":"John","code":123},"deputy":{"name":"Jack","code":234}}
Extended View - {"head":{"name":"John","code":123},"deputy":{"name":"Jack","code":234},"staff":[{"name":"Tom","code":345},{"name":"Matt","code":456}]}
Related
I am trying to read a JSON into the class. Jackson wants to apply a field of a subelement to the element itself, where it of course does not exist.
This is the JSON:
{
"authorizationRequest":{
"scope":["write","read"],
"resourceIds":["metadata"],
"approved":true,
"authorities":[],
"authorizationParameters":{
"scope":"write read",
"response_type":"token",
"redirect_uri":"",
"state":"",
"stateful":"false",
"client_id":"5102686_metadata"
},
"approvalParameters":{},
"state":"",
"clientId":"5102686_metadata",
"redirectUri":"",
"responseTypes":["token"],
"denied":false
},
"credentials":"",
"clientOnly":false,
"name":"testuser"
}
The classes look like the following:
// The main class that I try do deserialize:
public class DeserializedOAuth2Authentication extends OAuth2Authentication{
private String name;
private boolean clientOnly;
private AuthorizationRequest authorizationRequest = new DefaultAuthorizationRequest("", new ArrayList<>());
public DeserializedOAuth2Authentication() {
super(new DefaultAuthorizationRequest("", new ArrayList<>()), null);
}
#Override
#JsonProperty
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
#Override
#JsonProperty
public boolean isClientOnly() {
return clientOnly;
}
public void setClientOnly(boolean clientOnly) {
this.clientOnly = clientOnly;
}
#Override
#JsonProperty
public AuthorizationRequest getAuthorizationRequest() {
return authorizationRequest;
}
public void setAuthorizationRequest(AuthorizationRequest authorizationRequest) {
this.authorizationRequest = authorizationRequest;
}
}
AuthorizationRequest is an interface with all the getters for the listed elements; it is configured to be serialized by a DefaultAuthorizationRequest class also containing the respective setters and implementing fileds with corresponding names.
public class DefaultAuthorizationRequest implements AuthorizationRequest, Serializable {
private Set<String> scope = new LinkedHashSet<String>();
private Set<String> resourceIds = new HashSet<String>();
private boolean approved = false;
private Collection<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
private Map<String, String> authorizationParameters = new ConcurrentHashMap<String, String>();
private Map<String, String> approvalParameters = new HashMap<String, String>();
private String resolvedRedirectUri;
public Map<String, String> getAuthorizationParameters() {
return Collections.unmodifiableMap(authorizationParameters);
}
public Map<String, String> getApprovalParameters() {
return Collections.unmodifiableMap(approvalParameters);
}
public String getClientId() {
return authorizationParameters.get(CLIENT_ID);
}
public Set<String> getScope() {
return Collections.unmodifiableSet(this.scope);
}
public Set<String> getResourceIds() {
return Collections.unmodifiableSet(resourceIds);
}
public Collection<GrantedAuthority> getAuthorities() {
return Collections.unmodifiableSet((Set<? extends GrantedAuthority>) authorities);
}
public boolean isApproved() {
return approved;
}
public boolean isDenied() {
return !approved;
}
public String getState() {
return authorizationParameters.get(STATE);
}
public String getRedirectUri() {
return resolvedRedirectUri == null ? authorizationParameters.get(REDIRECT_URI) : resolvedRedirectUri;
}
public Set<String> getResponseTypes() {
return OAuth2Utils.parseParameterList(authorizationParameters.get(RESPONSE_TYPE));
}
public void setRedirectUri(String redirectUri) {
this.resolvedRedirectUri = redirectUri;
}
public void setScope(Set<String> scope) {
this.scope = scope == null ? new LinkedHashSet<String>() : new LinkedHashSet<String>(scope);
authorizationParameters.put(SCOPE, OAuth2Utils.formatParameterList(scope));
}
public void setResourceIds(Set<String> resourceIds) {
this.resourceIds = resourceIds == null ? new HashSet<String>() : new HashSet<String>(resourceIds);
}
public void setApproved(boolean approved) {
this.approved = approved;
}
public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
this.authorities = authorities == null ? new HashSet<GrantedAuthority>() : new HashSet<GrantedAuthority>(
authorities);
}
public void setAuthorizationParameters(Map<String, String> authorizationParameters) {
String clientId = getClientId();
Set<String> scope = getScope();
this.authorizationParameters = authorizationParameters == null ? new HashMap<String, String>()
: new HashMap<String, String>(authorizationParameters);
}
public void setApprovalParameters(Map<String, String> approvalParameters) {
this.approvalParameters = approvalParameters == null ? new HashMap<String, String>()
: new HashMap<String, String>(approvalParameters);
}
....
}
On calling read on the above JSON string I get an exception
com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "scope" (class de.mvbonline.vlx.auth.oauth2.DeserializedOAuth2Authentication), not marked as ignorable (3 known properties: "name", "authorizationRequest", "clientOnly"])
at [Source: (String)"{ "credentials":"", "clientOnly":false, "authorizationRequest":{ "scope":["write","read"], "resourceIds":["metadata"], "approved":true, "authorities":[], "authorizationParameters":{ "scope":"write read", "response_type":"token", "redirect_uri":"", "state":"", "stateful":"false", "[truncated 316 chars]; line: 1, column: 111] (through reference chain: de.mvbonline.vlx.auth.oauth2.DeserializedOAuth2Authentication["scope"])
Of course the field "scope" is not in the context of DeserializedOAuth2Authentication, but in the context of DefaultAuthorizationRequest. Why is Jackson searching in the wrong class for it?
I am unsing Jackson version 2.12.4
Make sure that DefaultAuthorizationRequest can be serialized and deserialized by Jackson. I guess that they are not for several reasons. Two that I can think of:
You have to let Jackson know how to deserialize DefaultAuthorizationRequest class. One possible solution would be to add a #JsonCreator and #JsonProperty to the class. The same applies to GrantedAuthority class.
DefaultAuthorizationRequest has fields of type Map, which need special attention. See these links on how to convert a JSON String to a Map<String, String> or, if the Map has custom objects, how to deserialize into a HashMap of custom objects
Also, you can take a look at Map Serialization and Deserialization with Jackson
I found my problem.
I formerly mapped my concrete implementation of the interface AuthorizationRequest via a handler:
mapper.addHandler(new DeserializationProblemHandler() {
#Override
public Object handleMissingInstantiator(DeserializationContext ctxt, Class<?> instClass, ValueInstantiator valueInsta, JsonParser p, String msg) throws IOException {
if(instClass.isAssignableFrom(AuthorizationRequest.class)) {
return new DeserializedAuthorizationRequest();
}
return super.handleMissingInstantiator(ctxt, instClass, valueInsta, p, msg);
}
});
This seems to be definitely not the same as annotating the field with the concrete class. This now works without problems:
public class DeserializedOAuth2Authentication extends OAuth2Authentication{
...
#Override
#JsonProperty("authorizationRequest")
#JsonDeserialize(as = DeserializedAuthorizationRequest.class)
public AuthorizationRequest getAuthorizationRequest() {
return authorizationRequest;
}
public void setAuthorizationRequest(AuthorizationRequest authorizationRequest) {
this.authorizationRequest = authorizationRequest;
}
}
I have a problem with parsing my custom response because the I have a response with Localization properties.
I am recieving a response that looks something like this:
[
{
"id": "dummyID1",
"name.en_US": "dummyNameEn1",
"name.fi_FI": "dummyNameFi1"
},
{
"id": "dummyID2",
"name.en_US": "dummyNameEn2",
"name.fi_FI": "dummyNameFi2"
},
{
"id": "dummyID3",
"name.en_US": "dummyNameEn3",
"name.fi_FI": "dummyNameFi3"
}...
]
And to parse that I have created a custom class Device.java:
public class Device {
public String id;
public LocalizedString name;
public Device(String id, LocalizedString name) {
this.id = id;
this.name = name;
}
//Getters and setters
}
Now here we have a custom object named LocalizedString.java:
public class LocalizedString implements Parcelable {
public static final Creator<LocalizedString> CREATOR = new Creator<LocalizedString>() {
#Override
public LocalizedString createFromParcel(Parcel in) {
return new LocalizedString(in);
}
#Override
public LocalizedString[] newArray(int size) {
return new LocalizedString[size];
}
};
private String en_US;
private String fi_FI;
public LocalizedString(String en, String fi) {
this.en_US = en;
this.fi_FI = fi;
}
protected LocalizedString(Parcel in) {
en_US = in.readString();
fi_FI = in.readString();
}
#Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(en_US);
dest.writeString(fi_FI);
}
//Getters, setters
}
Now in my response I want to create a list of Device's but it does not seem to understand how the ´LocalizedString´ works. Since my request is returning a <List<Device>> I cannot really customly parse it either.
Here is how I try to parse it:
Call<List<Device>> call = getMainActivity().getRestClient().getDevices();
call.enqueue(new Callback<List<Device>>() {
#Override
public void onResponse(Call<List<Device>> call, Response<List<Device>> response) {
if (isAttached()) {
if (response.isSuccessful()) {
// get data
List<Device> items = response.body();
}
}
}
#Override
public void onFailure(Call<List<Device>> call, Throwable t) {
if (isAttached()) {
Logger.debug(getClass().getName(), "Could not fetch installation document devices past orders", t);
getMainActivity().showError(R.string.error_network);
}
}
});
And:
#GET("document/devices")
Call<List<Device>> gettDevices();
What am I supposed to do in this situation to bind the name to the Device and later be able to either get en_US or fi_FI.
Better you can write it like this
public class Device {
#SerializedName("id")
public String id;
#SerializedName("name.en_US")
public String en;
#SerializedName("name.fi_FI")
public String fi;
public Device(String id, String english, String fi) {
this.id = id;
this.en = english;
this.fi = fi;
}
//Getters and setters
}
If you can control the source of the JSON, then a modification of that JSON structure is easy to solve your problem.
If you can not, the one way we can use to solve your problem is to use Jackson and custom deserializer:
public class DeviceDeserializer extends StdDeserializer<Device> {
public DeviceDeserializer() {
this(null);
}
public DeviceDeserializer(Class<?> vc) {
super(vc);
}
#Override
public Device deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
JsonNode node = jp.getCodec().readTree(jp);
String id = getStringValue(node, "id");
String en = getStringValue(node, "name.en_EN");
String fi = getStringValue(node, "name.fi_FI");
LocalizedString localized = new LocalizedString(en, fi);
return new Device(id, localizedString);
}
private String getStringValue(JsonNode node, String key) {
// Throws exception or use null is up to you to decide
return Optional.ofNullable(node.get("id"))
.map(JsonNode::asText)
.orElse(null);
}
}
Manually register the deserializer yourself or using the annotation:
#JsonDeserialize(using = DeviceDeserializer.class)
public class Device {
...
Note that you must enable retrofit jackson converter plugin: (see the Retrofit Configuration part)
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com")
.addConverterFactory(JacksonConverterFactory.create())
.build();
Read this: Get nested JSON object with GSON using retrofit
Consider the following json, getting from an public API:
anyObject : {
attributes: [
{
"name":"anyName",
"value":"anyValue"
},
{
"name":"anyName",
"value":
{
"key":"anyKey",
"label":"anyLabel"
}
}
]
}
As you can see, sometimes the value is a simple string and sometimes its an object. Is it somehow possible to deserialize those kind of json-results, to something like:
class AnyObject {
List<Attribute> attributes;
}
class Attribute {
private String key;
private String label;
}
How would I design my model to cover both cases. Is that possible ?
Despite being hard to manage as others have pointed out, you can do what you want. Add a custom deserializer to handle this situation. I rewrote your beans because I felt your Attribute class was a bit misleading. The AttributeEntry class in the object that is an entry in that "attributes" list. The ValueObject is the class that represents that "key"/"label" object. Those beans are below, but here's the custom deserializer. The idea is to check the type in the JSON, and instantiate the appropriate AttributeEntry based on its "value" type.
public class AttributeDeserializer extends JsonDeserializer<AttributeEntry> {
#Override
public AttributeEntry deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
JsonNode root = p.readValueAsTree();
String name = root.get("name").asText();
if (root.get("value").isObject()) {
// use your object mapper here, this is just an example
ValueObject attribute = new ObjectMapper().readValue(root.get("value").asText(), ValueObject.class);
return new AttributeEntry(name, attribute);
} else if (root.get("value").isTextual()) {
String stringValue = root.get("value").asText();
return new AttributeEntry(name, stringValue);
} else {
return null; // or whatever
}
}
}
Because of this ambiguous type inconvenience, you will have to do some type checking throughout your code base.
You can then add this custom deserializer to your object mapper like so:
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule();
simpleModule.addDeserializer(AttributeEntry.class, new AttributeDeserializer());
objectMapper.registerModule(simpleModule);
Here's the AttributeEntry:
public class AttributeEntry {
private String name;
private Object value;
public AttributeEntry(String name, String value) {
this.name = name;
this.value = value;
}
public AttributeEntry(String name, ValueObject attributes) {
this.name = name;
this.value = attributes;
}
/* getters/setters */
}
Here's the ValueObject:
public class ValueObject {
private String key;
private String label;
/* getters/setters */
}
I have a simple entity User.
public class User {
String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
And his corresponding DTO
public class UsuarioDTO {
String name;
String getName(){
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
I want to achieve something like I show below to avoid multiple classes of transformers.
#Dto(entity = "Usuario")
public class UsuarioDTO {
#BasicElement(name = "name")
String name;
String getName(){
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.FIELD)
public #interface BasicElement {
String name();
}
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.TYPE)
public #interface Dto {
String entity() default "";
}
With this example classes I can do:
public class Transformer {
public static void main(String[] args) {
UserDTO usuarioDTO = new UserDTO("Gabriel");
Class<UserDTO> obj = UserDTO.class;
if (obj.isAnnotationPresent(Dto.class)) {
Dto annotation = obj.getAnnotation(Dto.class);
Class<?> clazz;
try {
clazz = Class.forName(annotation.entity());
Constructor<?> constructor = clazz.getConstructor();
Object instance = constructor.newInstance();
for (Field originField : UserDTO.class.getDeclaredFields()) {
originField.setAccessible(true);
if (originField.isAnnotationPresent(BasicElement.class)) {
BasicElement basicElement = originField.getAnnotation(BasicElement.class);
Field destinationField = instance.getClass().getDeclaredField(basicElement.name());
destinationField.setAccessible(true);
destinationField.set(instance, originField.get(usuarioDTO));
}
}
System.out.println(((User) instance).getName());
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
But this would be to expensive because consumes the annotations in each transformation.
It's possible with Byte-buddy to read the annotations and create a class transformer whose decompiled code look like this:
public class TransformerImpl implements ITransformer{
public Object toEntity(Object dto){
User user = new User();
user.setName(dto.getName());
}
}
UPDATE:
#Rafael Winterhalter, something like this?
public class Transformer<D,E> {
List<Field> dtoFields = new ArrayList<Field>();
Constructor<D> dtoConstructor;
List<Field> entityFields = new ArrayList<Field>();
Constructor<E> entityConstructor;
public Transformer(Class<D> dtoClass){
try {
Dto annotation = dtoClass.getAnnotation(Dto.class);
Class<E> entityClass = (Class<E>) annotation.entity();
//entityConstructor = entityClass.getConstructor();
entityConstructor = entityClass.getDeclaredConstructor();
entityConstructor.setAccessible(true);
dtoConstructor = dtoClass.getConstructor();
dtoConstructor.setAccessible(true);
lookupFields(entityClass, dtoClass);
} catch (Exception e) {
e.printStackTrace();
}
}
private void lookupFields(Class<E> entityClass, Class<D> dtoClass) throws NoSuchFieldException {
for (Field dtoField : dtoClass.getDeclaredFields()) {
if (dtoField.isAnnotationPresent(BasicElement.class)) {
BasicElement basicElement = dtoField.getAnnotation(BasicElement.class);
String entityFieldName = (basicElement.name().equals("")) ? dtoField.getName() : basicElement.name();
Field entityField = entityClass.getDeclaredField(entityFieldName);
dtoField.setAccessible(true);
entityField.setAccessible(true);
dtoFields.add(dtoField);
entityFields.add(entityField);
}
}
}
public E toEntity(D dto) throws ReflectiveOperationException {
E entity = entityConstructor.newInstance();
for (int i = 0; i < entityFields.size(); i++){
Field destination = entityFields.get(i);
Field origin = dtoFields.get(i);
destination.set(entity, origin.get(dto));
}
return entity;
}
public D toDto(E entity) throws ReflectiveOperationException {
D dto = dtoConstructor.newInstance();
for (int i = 0; i < entityFields.size(); i++){
Field origin = entityFields.get(i);
Field destination = dtoFields.get(i);
destination.set(dto, origin.get(entity));
}
return dto;
}
}
To answer your question: Yes, it is possible. You can ask Byte Buddy to create instances of ITransformer for you where you implement the only method to do what you want. You would however need to implement your own Implementation instance for doing so.
However, I would not recommend you to do so. I usually tell users that Byte Buddy should not be used for performance work and for a majority of use-cases, this is true. Your use case is one of them.
If you implemented classes, you would have to cache these classes for any mapping. Otherwise, the class generation-costs would be the significant share. Instead, you rather want to maintain a transformer that caches the objects of the reflection API (reflective lookups are the expensive part of your operation, reflective invocation is not so problematic) and reuses previously looked-up values. This way, you gain on performance without dragging in code generation as another (complex) element of your application.
I'm looking for possibility to serialize transient information only in some cases:
#JsonInclude(Include.NON_NULL)
#Entity
public class User {
public static interface AdminView {}
... id, email and others ...
#Transient
private transient Details details;
#JsonIgnore // Goal: ignore all the time, except next line
#JsonView(AdminView.class) // Goal: don't ignore in AdminView
public Details getDetails() {
if (details == null) {
details = ... compute Details ...
}
return details;
}
}
public class UserDetailsAction {
private static final ObjectWriter writer = new ObjectMapper();
private static final ObjectWriter writerAdmin = writer
.writerWithView(User.AdminView.class);
public String getUserAsJson(User user) {
return writer.writeValueAsString(user);
}
public String getUserAsJsonForAdmin(User user) {
return writerAdmin.writeValueAsString(user);
}
}
If I call getUserAsJson I expected to see id, email and other fields, but not details. This works fine. But I see same for getUserAsJsonForAdmin, also without detail. If I remove #JsonIgnore annotation - I do see details in both calls.
What do I wrong and is there good way to go? Thanks!
You may find the use of the dynamic Jackson filtering slightly more elegant for your use case. Here is an example of the filtering of POJO fields based on a custom annotation sharing one object mapper instance:
public class JacksonFilter {
static private boolean shouldIncludeAllFields;
#Retention(RetentionPolicy.RUNTIME)
public static #interface Admin {}
#JsonFilter("admin-filter")
public static class User {
public final String email;
#Admin
public final String details;
public User(String email, String details) {
this.email = email;
this.details = details;
}
}
public static class AdminPropertyFilter extends SimpleBeanPropertyFilter {
#Override
protected boolean include(BeanPropertyWriter writer) {
// deprecated since 2.3
return true;
}
#Override
protected boolean include(PropertyWriter writer) {
if (writer instanceof BeanPropertyWriter) {
return shouldIncludeAllFields || ((BeanPropertyWriter) writer).getAnnotation(Admin.class) == null;
}
return true;
}
}
public static void main(String[] args) throws JsonProcessingException {
User user = new User("email", "secret");
ObjectMapper mapper = new ObjectMapper();
mapper.setFilters(new SimpleFilterProvider().addFilter("admin-filter", new AdminPropertyFilter()));
System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(user));
shouldIncludeAllFields = true;
System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(user));
}
}
Output:
{
"email" : "email"
}
{
"email" : "email",
"details" : "secret"
}
It's look like jackson have horrible concept on very cool feature like #JsonView. The only way I discover to solve my problem is:
#JsonInclude(Include.NON_NULL)
#Entity
public class User {
public static interface BasicView {}
public static interface AdminView {}
... id and others ...
#JsonView({BasicView.class, AdminView.class}) // And this for EVERY field
#Column
private String email;
#Transient
private transient Details details;
#JsonView(AdminView.class)
public Details getDetails() {
if (details == null) {
details = ... compute Details ...
}
return details;
}
}
public class UserDetailsAction {
private static final ObjectWriter writer = new ObjectMapper()
.disable(MapperFeature.DEFAULT_VIEW_INCLUSION)
.writerWithView(User.BasicView.class);
private static final ObjectWriter writerAdmin = new ObjectMapper()
.disable(MapperFeature.DEFAULT_VIEW_INCLUSION)
.writerWithView(User.AdminView.class);
public String getUserAsJson(User user) {
return writer.writeValueAsString(user);
}
public String getUserAsJsonForAdmin(User user) {
return writerAdmin.writeValueAsString(user);
}
}
Maybe it's help some one. But I hope to find better solution and because doesn't accept my own answer.
EDIT: because interface can extends (multiple) interfaces, I can use:
public static interface AdminView extends BasicView {}
and just
#JsonView(BasicView.class)
instead of
#JsonView({BasicView.class, AdminView.class})