javaspringspring-boot

How do I allow null values in a POST request body? Java and Spring Framework


I have a VideoGame record:

public record VideoGame(
        @Positive
        Integer id,
        @NotEmpty
        String title,
        @NotEmpty
        String console,
        @NotEmpty
        String genre,
        String publisher,
        String developer,
        Double critic_score,
        Double other_sales,
        LocalDate release_date,
        LocalDate last_update
) { }

A VideoGame repository:

@Repository
public class VideoGameRepository {

    private final JdbcClient jdbcClient;

    public VideoGameRepository(JdbcClient jdbcClient) {
        this.jdbcClient = jdbcClient;
    }

    public void create(VideoGame videoGame) {
        var newVideoGame = jdbcClient.sql("INSERT INTO videogamesalestest (id, title, console, genre, publisher, developer, critic_score, release_date, last_update)" +
                "VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
                .param(List.of(videoGame.id(), videoGame.title(), videoGame.console(), videoGame.genre(), videoGame.publisher(), videoGame.developer(), videoGame.critic_score(), videoGame.release_date(), videoGame.last_update()))
                .update();

        Assert.state(newVideoGame == 1, "Failed to insert new videoGame");
    }
}

and a controller for this:

@RestController
@RequestMapping("/api/videogames")
public class VideoGameController {

    private final VideoGameRepository videoGameRepository;

    public VideoGameController(VideoGameRepository videoGameRepository) {
        this.videoGameRepository = videoGameRepository;
    }

    @ResponseStatus(HttpStatus.CREATED)
    @PostMapping("")
    void create(@RequestBody VideoGame videoGame) {
        videoGameRepository.create(videoGame);
    }
}

I want to allow the client to submit a POST request that may or may not contain all of the fields of a VideoGame record.

I tried just setting the field value to null in the JSON body as such, and then sending the request:

{
  "id": 1,
  "title": "NewGame",
  "console": "Xbox",
  "genre": "Action",
  "publisher": "CBGAMES",
  "developer": "CBGAMES",
  "critic_score": 10.0,
  "release_date": "2025-01-07",
  "last_update": null
}

but I get a status 500 response "Internal Server Error".

I've also tried annotating the fields in the record with @Nullable, but I get a warning in the repository that the argument might be null.

What can I do to acheive this? Will I need to make a class instead of a record to handle null values or is there a more "Springy" way to accomplish. I also considered using Optionals, but I've read that this isn't really what Optional were meant acheive. I also don't want to restrict the client from passing in a complete record, since it doesn't make sense in my case.

Update: My question has been resolved. The core issue I was facing (ignoring the syntactical problems with my code) was how do I allow null values, or lack of values, be passed from my request body and deserialized into a VideoGame object. The solution was to remove the call to List.of() in my params() call. The List.of() API clearly states it throws a NullPointException if an element is null. Removing the List.of() and just passing the params was the fix. Interestingly, I didn't even need to @Nullable the record fields, and I didn't need @Valid in my controller.


Solution

  • There are several things wrong with your code.

    1. Your query needs a whitespace before the VALUES element to have a proper query.
    2. Your query identifies 9 columns in the INSERT clause, while providing 14 placeholders, those numbers should match. So either add additional columns or remove placeholders.
    3. List.of while throw a NullPointerException if an element is null. So don't use List.of. The JdbcClient has a param method that simply supports varargs, so no need to wrap things in a List.
    @Repository
    public class VideoGameRepository {
    
        private final JdbcClient jdbcClient;
    
        public VideoGameRepository(JdbcClient jdbcClient) {
            this.jdbcClient = jdbcClient;
        }
    
        public void create(VideoGame videoGame) {
            var newVideoGame = jdbcClient.sql("INSERT INTO videogamesalestest (id, title, console, genre, publisher, developer, critic_score, release_date, last_update) " +
                    "VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)")
                    .param(videoGame.id(), videoGame.title(), videoGame.console(), videoGame.genre(), videoGame.publisher(), videoGame.developer(), videoGame.critic_score(), videoGame.release_date(), videoGame.last_update())
                    .update();
    
            Assert.state(newVideoGame == 1, "Failed to insert new videoGame");
        }
    }
    

    Something like the above should make things work. It has an additional after the INSERT ... stuff. The number of placeholders has been reduced and it now uses the param method with var-args instead of List.of.