There is a Spring approach to filter out fields from a service response with JSON views, but i am missing an equivalent approach to enrich a response with some dynamic/syntetic fields like this;
class User{
getFirstName(){...}
getLastName(){...}
getCreateDate(){...}
}
class UserViewA{
getFullName(){
return getLastName()+", "+getFirstName()
}
getCreateDate(){...}
}
class UserViewB{
getFullName(){
return getFirstName()+" "+getLastName()
}
getCreateDate(){...}
}
I could wrap the user within the view, but I do not want to propagate all needed user fields manually.
My other idea was to extend the views with the user object and create some sort of an reference linker to copy values references from the user object to the view, but this will get complicated with collections.
Is there some other approach or framework to achieve this? Is this concept not addressed in any way at all?
Update:
Clarification by example:
- I do not want to wrap the User object because I do not want to maintain same getter methods from User class in different UserView objects.
- I can not extend the User because itsis a domain object loaded from other resource.
- There should be no reference to different UserView objects in the User object.
I am looking for a kind of facade solution/framework/approach.
How about using jackson @JsonUnwrapped
?
http://fasterxml.github.io/jackson-annotations/javadoc/2.0.0/com/fasterxml/jackson/annotation/JsonUnwrapped.html
public class UserViewA {
@JsonUnwrapped
private User user;
public User getUser() ...
public String getFullName() {
return user.getFirstName() + " " + user.getLastName()
}
}
JsonUnwrapped will just pull all properties of User to the root level and still have the own properties of UserViewA in there.
When you cannot modify the domain object's class, you can enrich your JSON with "virtual" fields using a mix-in.
For example, you could create a class named UserMixin
that hides the firstName
and lastName
fields and that exposes a virtual fullName
field:
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.annotation.JsonAppend;
import java.util.Date;
@JsonAppend(
prepend = true,
props = {
@JsonAppend.Prop(name = "fullName", value = UserFullName.class)
})
public abstract class UserMixin
{
@JsonIgnore
public abstract String getFirstName();
@JsonIgnore
public abstract String getLastName();
public abstract Date getCreatedDate();
}
Then you would implement a class named UserFullName
that extends VirtualBeanPropertyWriter
to provide the virtual field's value:
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
import com.fasterxml.jackson.databind.introspect.AnnotatedClass;
import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition;
import com.fasterxml.jackson.databind.ser.VirtualBeanPropertyWriter;
import com.fasterxml.jackson.databind.util.Annotations;
public class UserFullName extends VirtualBeanPropertyWriter
{
public UserFullName() {}
public UserFullName(BeanPropertyDefinition propDef, Annotations contextAnnotations, JavaType declaredType)
{
super(propDef, contextAnnotations, declaredType);
}
@Override
protected Object value(Object bean, JsonGenerator gen, SerializerProvider prov) throws Exception
{
return ((User) bean).getFirstName() + " " + ((User) bean).getLastName();
}
@Override
public VirtualBeanPropertyWriter withConfig(MapperConfig<?> config, AnnotatedClass declaringClass, BeanPropertyDefinition propDef, JavaType type)
{
return new UserFullName(propDef, null, type);
}
}
Finally, you would need to register your mix-in with the ObjectMapper as shown in the following JUnit test:
@Test
public void testUserFullName() throws IOException
{
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.addMixIn(User.class, UserMixin.class);
System.out.println(objectMapper.writeValueAsString(new User("Frodo", "Baggins")));
}
The output is then:
{"fullName":"Frodo Baggins","createdDate":1485036953535}
Actually, the best way to do that is wrap the User object (if your serialization framework uses getter method) or rewrite whole object to another view object (if your serialization framework uses property). If you don't want rewrite your object manually, you can use framework like dozer or other Java Bean mapping framework.
If your model look like this:
public class User {
private final String firstName;
private final String lastName;
// constructor and getter method
}
Your wrap class could look like this:
public class UserView {
private final User user;
public UserView(User user) {
this.user = user;
}
public String getFullName() {
return user.getFirstName() + " " + user.getLastName();
}
}
Or also you can rewrite whole object:
public class UserView {
private final String fullName;
public UserView(User user) {
this.fullName = user.getFirstName() + " " + user.getLastName();
}
// getter if needed
}
How'bout this?
@Transient
@JsonProperty("fullName")
getFullName(){
return getLastName()+", "+getFirstName()
}
Update based on your comment:
An approach I'd suggest is to create a UserView class which will use extension or composition (I think in this case extension would be ok). On each additional method (getFullName) you can apply specific view strategy manually based on your needs.
If you don't have sophisticated requirements, you can add all additional methods in the UserView (getFullNameA, getFullNameB) and annotate them with different JsonViews.
If you need to have one method (getFullName) instead of two with different name, you can have a getFullName which returns Arrays.asList(getFullNameA(), getFullNameB()).get(0))
and annotate the getFullNameA() and getFullNameB() with different Json View annotations. This way your list is going to have always one item, depending on the view used.