23

I've been going through my head the best way to design a JSON API using Spring MVC. As we all know IO is expensive, and thus I don't want to make the client make several API calls to get what they need. However at the same time I don't necessarily want to return the kitchen sink.

As an example I was working on a game API similar to IMDB but for video games instead.

If I returned everything connected to Game it would look something like this.

/api/game/1

{
    "id": 1,
    "title": "Call of Duty Advanced Warfare",
    "release_date": "2014-11-24",
    "publishers": [
        {
            "id": 1,
            "name": "Activision"
        }
    ],
    "developers": [
        {
            "id": 1,
            "name": "Sledge Hammer"
        }
    ],
    "platforms": [
        {
            "id": 1,
            "name": "Xbox One",
            "manufactorer": "Microsoft",
            "release_date": "2013-11-11"
        },
        {
            "id": 2,
            "name": "Playstation 4",
            "manufactorer": "Sony",
            "release_date": "2013-11-18"
        },
        {
            "id": 3,
            "name": "Xbox 360",
            "manufactorer": "Microsoft",
            "release_date": "2005-11-12"
        }
    ],
    "esrbRating": {
        "id": 1,
        "code": "T",
        "name": "Teen",
        "description": "Content is generally suitable for ages 13 and up. May contain violence, suggestive themes, crude humor, minimal blood, simulated gambling and/or infrequent use of strong language."
    },
    "reviews": [
        {
            "id": 1,
            "user_id": 111,
            "rating": 4.5,
            "description": "This game is awesome"
        }
    ]
}

However they may not need all this information, but then again they might. Making calls for everything seems like a bad idea from I/O and performance.

I thought about doing it by specifying include parameter in the requests.

Now for example if you did not specify any includes all you would get back is the following.

{
    "id": 1,
    "title": "Call of Duty Advanced Warfare",
    "release_date": "2014-11-24"
}

However it you want all the information your requests would look something like this.

/api/game/1?include=publishers,developers,platforms,reviews,esrbRating

This way the client has the ability to specify how much information they want. However I'm kind of at a loss the best way to implement this using Spring MVC.

I'm thinking the controller would look something like this.

public @ResponseBody Game getGame(@PathVariable("id") long id, 
    @RequestParam(value = "include", required = false) String include)) {

        // check which include params are present

        // then someone do the filtering?
}

I'm not sure how you would optionally serialize the Game object. Is this even possible. What is the best way to approach this in Spring MVC?

FYI, I am using Spring Boot which includes Jackson for serialization.

greyfox
  • 6,426
  • 23
  • 68
  • 114
  • 2
    Seems like you do some premature optimization here. Is there really so much data in your entity that you need to filter it on the client's request? Based on what you've shown, you will overly complicate both client and server and break the RESTfullness of your service, while not saving much IO. – Forketyfork May 31 '15 at 14:58
  • In the case of my example, I agree this is definitely overkill. Lets just say for example sake though that returns the Game object would result in a huge JSON object, are you saying it would be better to do /game/1/reviews, instead of /game/1?include=reviews? – greyfox May 31 '15 at 15:06
  • if the object is huge, then I would request the collections from separate subresourses, because the overhead of issuing several requests would be small anyway in relation to total volume of transferred data. – Forketyfork May 31 '15 at 15:10
  • https://stackoverflow.com/questions/23101260/ignore-fields-from-java-object-dynamically-while-sending-as-json-from-spring-mvc/49207551#49207551 – Hett Mar 10 '18 at 09:46
  • Just curious, have you tried using @JsonFilter? – ritz Feb 28 '22 at 17:39

5 Answers5

19

Instead of returning a Game object, you could serialize it as as a Map<String, Object>, where the map keys represent the attribute names. So you can add the values to your map based on the include parameter.

@ResponseBody
public Map<String, Object> getGame(@PathVariable("id") long id, String include) {

    Game game = service.loadGame(id);
    // check the `include` parameter and create a map containing only the required attributes
    Map<String, Object> gameMap = service.convertGameToMap(game, include);

    return gameMap;

}

As an example, if you have a Map<String, Object> like this:

gameMap.put("id", game.getId());
gameMap.put("title", game.getTitle());
gameMap.put("publishers", game.getPublishers());

It would be serialized like this:

{
  "id": 1,
  "title": "Call of Duty Advanced Warfare",
  "publishers": [
    {
        "id": 1,
        "name": "Activision"
    }
  ]
}
Marlon Bernardes
  • 13,265
  • 7
  • 37
  • 44
  • 3
    Doing this with a custom service doesn't sound like a very good idea to me, is there any "Spring" way of doing this? – EralpB Jan 23 '17 at 09:46
  • @EralpB you could also search how to serialise fields based on their name (using Jackson) or use something like GraphQL. Other than that, I'm not sure if there is a "spring" way to achieve the same result. – Marlon Bernardes Jan 23 '17 at 13:18
  • How can we convert Game class to Map? – Kabindra Shrestha Jun 14 '17 at 05:51
  • Is it possible to provide an example or expand the answer with how to implement the 'service.convertGameToMap(game, include)' method? What is the best practice to include it, etc. – Matti VM Sep 19 '19 at 06:52
9

Being aware that my answer comes quite late: I'd recommend to look at Projections.

What you're asking for is what projections are about.

Since you're asking about Spring I'd give this one a try: https://docs.spring.io/spring-data/rest/docs/current/reference/html/#projections-excerpts

A very dynamic way for providing different projections on demand is offered by GraphQL. I just came across a very helpful article about how to use GraphQL with SpringBoot: https://www.graphql-java.com/tutorials/getting-started-with-spring-boot/

yaccob
  • 1,230
  • 13
  • 16
  • I don't think this answers OP's question. First, you'd need to use Sprint Data Rest (which has not been specified by OP) meaning exposing Spring `@Repository` directly. Also, how do you handle which projection to return dynamically ? AFAIK, `Projection` is just the way that Spring Data Rest provides to handle DTO/client representation but does not handle the dynamic part of resource expansion – Blockost Mar 13 '22 at 17:51
  • @Blockost: `Projection` is the technical term for the mapping of a set into a subset. In the first instance this term is not coupled to the Spring framework at all. Since the OP explicitly mentioned the Spring context I also added a link to the Spring implementation of projections. Not more and not less. – yaccob Mar 14 '22 at 19:07
1

This can be done by Spring Projections. Also works fine with Kotlin. Take a look here: https://www.baeldung.com/spring-data-jpa-projections

mleister
  • 1,697
  • 2
  • 20
  • 46
1

Looks there is always quite a lot of manual work. If you use some persistence abstraction you can have less work compared to plain SpringJDBC (JdbcTemplate). Also depends if your model is aligned with database column names. There are nice series about Query Languages e.g. QueryDSL: https://www.baeldung.com/rest-search-language-spring-jpa-criteria.

Using SpringRest & QueryDSL you can end up with something like this:

Rest controller:

//...
import com.querydsl.core.types.Path;
import com.querydsl.core.types.dsl.BooleanExpression;
//...
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
//...

@ApiOperation("Returns list of all users")
@GetMapping(value = "/users", produces = {MediaType.APPLICATION_JSON_VALUE})
@ResponseStatus(HttpStatus.OK)
public Page<UsersRest> getUsers(
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "10") int size,
        @RequestParam(defaultValue = "userId,desc") String[] sort,
        @RequestParam(required = false) Optional<String> search,
        @RequestParam(required = false) Optional<String> fields) {

    Sort sorting = parser.parseSortingParameters(sort);
    PageRequest pageable = PageRequest.of(page, size, sorting);
    // search
    BooleanExpression searchPredicate = parser.parseSearchParameter(search);
    // requested columns
    Path[] columns = parser.parseFieldsParameter(fields);

    Page<User> userPage = userService.getAllUsers(pageable, searchPredicate, columns);

    return new PageImpl<>(userPage, userPage.getPageable(), userPage.getTotalElements());
}

Repository class:

//...
import com.querydsl.core.QueryResults;
import com.querydsl.core.Tuple;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.Path;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.sql.Configuration;
import com.querydsl.sql.SQLQuery;
import com.querydsl.sql.SQLQueryFactory;
import com.querydsl.sql.spring.SpringConnectionProvider;
import com.querydsl.sql.spring.SpringExceptionTranslator;
//...
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
//...

@Transactional(readOnly = true)
public Page<User> findAll(Pageable pageable, BooleanExpression searchPredicate, Path[] columns) {
    final var userTable = new QUser("USER");

    // Alternatively (if column names are aligned with field names - so manual mapping is not needed) can be used
    // Expressions.path constructor to dynamically create path:
    // http://www.querydsl.com/static/querydsl/latest/reference/html/ch03.html
    OrderSpecifier<?>[] order = convertToDslOrder(pageable.getSort());

    SQLQuery<Tuple> sql = queryFactory
            .select(columns)
            .from(userTable)
            .where(searchPredicate)
            .orderBy(order);

    sql.offset(pageable.getPageNumber());
    sql.limit(pageable.getPageSize());

    QueryResults<Tuple> queryResults = sql.fetchResults();

    final long totalCount = queryResults.getTotal();
    List<Tuple> results = queryResults.getResults();
    List<User> users = userRowMapper(userTable, results);

    return new PageImpl<>(users, pageable, totalCount);
}
sasynkamil
  • 859
  • 2
  • 12
  • 23
-1

Solution 1: Add @JsonIgnore to the variable you dont want to include in API response (in the model)

@JsonIgnore
    private Set<Student> students;

Solution 2: Remove the getters for the variables you don't want included.

If you need them else where, use different format for the getters so spring doesn't know about it.