18

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.

Roman K
  • 3,309
  • 1
  • 33
  • 49

4 Answers4

13

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.

Numyx
  • 1,758
  • 2
  • 11
  • 16
5

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}
1

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
}
Community
  • 1
  • 1
Mateusz Korwel
  • 1,118
  • 1
  • 8
  • 14
-1

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.

Chris
  • 549
  • 1
  • 5
  • 18
  • This will only work for ViewA, not ViewB. Further I do not want to bloat my domain object with view specific logic. Edit: this question is more about a concept, not this specific use case. – Roman K Jul 03 '15 at 10:33
  • Based on your edit: Thank you, but 1. Wrapping the user causes additional maintenance. 2. I can not extend a domain object. – Roman K Jul 06 '15 at 13:47
  • You can apply the additional view strategies by using AspectJ and ITD – Chris Jul 06 '15 at 23:10
  • An alternative to adding json view functionality to your domain object through ITD, could be creating the UserView with composition (declaring a private User instance) and delegate User methods to User class using lombok. This way you don't have to pollute your UserView with User methods. See https://projectlombok.org/features/Delegate.html – Chris Jul 17 '15 at 14:05