How to wrap JSON response from Spring REST repository?
Asked Answered
I

2

10

I have a spring REST controller which returns the following JSON payload:

[
  {
    "id": 5920,
    "title": "a title"
  },
  {
    "id": 5926,
    "title": "another title",
  }
]

The REST controller with its corresponding get request method:

@RequestMapping(value = "example")
public Iterable<Souvenir> souvenirs(@PathVariable("user") String user) {
    return new souvenirRepository.findByUserUsernameOrderById(user);
}

Now the Souvenir class is a pojo:

@Entity
@Data
public class Souvenir {

    @Id
    @GeneratedValue
    private long id;

    private String title;

    private Date date;
}

Regarding https://www.owasp.org/index.php/OWASP_AJAX_Security_Guidelines#Always_return_JSON_with_an_Object_on_the_outside and http://haacked.com/archive/2009/06/25/json-hijacking.aspx/ I would like to wrap the response within an object so that it is not vulnerable to attacks. Of course I could do something like this:

@RequestMapping(value = "example")
public SouvenirWrapper souvenirs(@PathVariable("user") String user) {
    return new SouvenirWrapper(souvenirRepository.findByUserUsernameOrderById(user));
}

@Data
class SouvenirWrapper {
  private final List<Souvenir> souvenirs;

  public SouvenirWrapper(List<Souvenir> souvenirs) {
    this.souvenirs = souvenirs;
  }
}

This results in the following JSON payload:

   {
     "souvenirs": [
        {
          "id": 5920,
          "title": "a title"
        },
        {
          "id": 5926,
          "title": "another title",
        }
    ]
  }

This helps in preventing some JSON/Javascript attacks but I don't like the verbosity of the Wrapper class. I could of course generalize the above approach with generics. Is there another way to achieve the same result in the Spring ecosystem (with an annotation or something similar)? An idea would be that the behaviour is done by Spring automatically, so whenever there is a REST controller that returns a list of objects, it could wrap those objects within an object wrapper so that no direct list of objects get serialized?

Icsh answered 29/10, 2016 at 20:18 Comment(3)
Spring alternative? What are you talking? Struts has it, Spring not. Elaborate your question. Only wrapper is hat Spring has to you or you may extend a messages on the fly if em not finale.Factorial
Here you go: docs.spring.io/spring/docs/current/javadoc-api/org/…. In your own implementation you can wrap it into whatever structure you need.Kariekaril
What an interesting question.. You can try to play with @ControllerAdvice to return your generic wrapper if actual value is iterable, or you can try to highjack deserializer, probably Jackson Http message converter in your case. Keep us posted :)Baffle
I
13

I ended up with the following solution (thanks to @vadim-kirilchuk):

My controller still looks exactly as before:

@RequestMapping(value = "example")
public Iterable<Souvenir> souvenirs(@PathVariable("user") String user) {
    return new souvenirRepository.findByUserUsernameOrderById(user);
}

I added the following implementation of ResponseBodyAdvice which basically gets executed when a controller in the referenced package tries to respond to a client call (to my understanding):

@ControllerAdvice(basePackages = "package.where.all.my.controllers.are")
public class JSONResponseWrapper implements ResponseBodyAdvice {
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }

    @Override
    @SuppressWarnings("unchecked")
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        if (body instanceof List) {
            return new Wrapper<>((List<Object>) body);
        }
        return body;
    }

    @Data // just the lombok annotation which provides getter and setter
    private class Wrapper<T> {
        private final List<T> list;

        public Wrapper(List<T> list) {
            this.list = list;
        }
    }
}

So with this approach I can keep my existing method signature in my controller (public Iterable<Souvenir> souvenirs(@PathVariable("user") String user)) and future controllers don't have to worry about wrapping its Iterables within such a wrapper because the framework does this part of the work.

Icsh answered 30/10, 2016 at 20:24 Comment(1)
Glad it helped you. One thing to note here: that's ok fo security reasons, but it may be very useful if you will create wrappers for collections manually and for example provide a size of collection. {size:10, souvenirs:[..]} There are other possibilities when you create a wrapper object manually.Baffle
C
1

Based in your solution I ended up with a more flexible option. First I created an annotation to activate the behaviour whenever I want and with a customizable wrapper attribute name:

@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface JsonListWrapper {
    String name() default "list";
}

This annotation can be used on the entity class so it's applied to all controllers responses of List<MyEntity> or can be used for specifics controller methods.

The ControllerAdvice will look like this (note that I return a Map<Object> to dynamically set the wrapper name as a map key).

public class WebResponseModifierAdvice implements ResponseBodyAdvice<Object> {

    @Override
    public boolean supports(final MethodParameter returnType, final Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(final Object body,
                                  final MethodParameter returnType,
                                  final MediaType selectedContentType,
                                  final Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                  final ServerHttpRequest request,
                                  final ServerHttpResponse response) {
        
        if (body instanceof List && selectedContentType.isCompatibleWith(MediaType.APPLICATION_JSON)) {
            return checkListWrapper(body, returnType);
        } else {
            return body;
        }
    }
    
    /**
     * Detects use of {@link JsonListWrapper} in a response like <tt>List&lt;T&gt;</tt>
     * in case it's necesary to wrap the answer.
     *   
     * @param body body to be written in the response
     * @param returnType controller method return type
     * @return
     */
    private Object checkListWrapper(final Object body,
                                    final MethodParameter returnType) {

        String wrapperName =  null; 
        try {
            // Checks class level annotation (List<C>).
            String typeName = "";
            String where = "";
            String whereName = "";

            // Gets generic type List<T>
            Type[] actualTypeArgs = ((ParameterizedType) returnType.getGenericParameterType()).getActualTypeArguments();
            if (actualTypeArgs.length > 0) {
                Type listType = ((ParameterizedType) returnType.getGenericParameterType()).getActualTypeArguments()[0];
                if (listType instanceof ParameterizedType) {
                    Type elementType = ((ParameterizedType) listType).getActualTypeArguments()[0];
                    elementType.getClass();
                    try {
                        typeName = elementType.getTypeName();
                        Class<?> clz = Class.forName(typeName);
                        JsonListWrapper classListWrapper = AnnotationUtils.findAnnotation(clz, JsonListWrapper.class);
                        if (classListWrapper != null) {
                            where = "clase";
                            whereName = typeName;
                            wrapperName = classListWrapper.name();
                        }
                    } catch (ClassNotFoundException e) {
                        log.error("Class not found" + elementType.getTypeName(), e);
                    }
                }
            }
            
            // Checks method level annotations (prevails over class level)
            JsonListWrapper methodListWrapper = AnnotationUtils.findAnnotation(returnType.getMethod(), JsonListWrapper.class);
            if (methodListWrapper != null) {
                where = "método";
                whereName = returnType.getMethod().getDeclaringClass() + "." + returnType.getMethod().getName() + "()";
                wrapperName = methodListWrapper.name();
            }
            
            if (wrapperName != null) {
                if (log.isTraceEnabled()) {
                    log.trace("@JsonListWrapper detected {} {}. Wrapping List<{}> in \"{}\"", where, whereName, typeName, wrapperName);
                }
                final Map<String, Object> map = new HashMap<>(1);
                map.put(wrapperName, body);
                return map;
            }
        } catch(Exception ex) {
            log.error("Error getting type of List in the response", ex);
        }
    
        return body;
    }
}

This way you can use either:

@JsonListWrapper(name = "souvenirs")
public class Souvenir {
  //...class members
}

...or

@JsonListWrapper(name = "souvenirs")
@RequestMapping(value = "example")
public ResponseEntity<List<Souvenir>> souvenirs(@PathVariable("user") String user) {
    return new souvenirRepository.findByUserUsernameOrderById(user);
}
Crepitate answered 4/7, 2022 at 7:37 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.