简体   繁体   中英

Spring Rest Controller Return Specific Fields

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.

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"
    }
  ]
}

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/

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

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 eg 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);
}

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM