I have REST
api, and when client call POST
request with body, backend after deserialize should distinguish null
from the absence of a value.
Because if value in JSON
is null, then value
in DB should become null.
If value in JSON
absence, then value
in DB should remain unchanged.
JSON:
{
"id" : 1,
"name" : "sample name",
"value" : null
}
OR
{
"id" : 1,
"name" : "sample name"
}
For Java after deserialization it is look like : value = null;
Java:
@Entity
@Table("sample")
public class Sample {
@Id
@Column
private Long id;
@Column
private String name;
@Column
private Integer value;
// getters / setters
}
Sample REST request:
@PutMapping
public ResponseEntity<SampleDto> updateSample(@RequestBody SampleDto dto) {
return ResponseEntity.ok(service.updateSample(dto));
}
Sample service impl:
public SampleDto updateSample(SampleDto dto) {
Sample sample = sampleRepository.findById(dto.getId);
sample.setName(dto.getName());
sample.setValue(dto.getValue());
//In this operation back need understand: value is null or absence
//Because if value in JSON is null, then value in DB should become null
//If value in JSON absence, then value in DB should remain unchanged
Sample newSample = sampleRepository.save(sample);
return modelMapper.map(newSample, SampleDto.class);
}
Project use Spring Data
.
Maybe I should use @JsonDeserialize
annotation or other Hibernate
annotation
I tried use @JsonDeserialize
, but it is not solution.
Partial update is different from full-resource update and we should implement it in a different way. Let's create two request POJO
classes. One class will be used to create and update resources, second will be used to partially update given resource. To emphasise it we will use different HTTP
methods. To distinguish null
from absence
we can use java.util.Optional
class.
SampleCompleteRequest
class we use together with POST
(create) and PUT
(update) methods.SamplePartialRequest
class we use together with PATCH
(partially update) method.To avoid boilerplate code in this example I'm using Lombok and MapStruct but it is not required.
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class SampleCompleteRequest {
@NotBlank
private String name;
private String value;
}
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.util.Optional;
@Data
public class SamplePartialRequest {
private Optional<@NotBlank String> name;
private Optional<String> value;
}
import lombok.Data;
@Data
public class SampleResponse {
private Long id;
private String name;
private String value;
}
import lombok.Data;
@Data
public class Sample {
//@Id - Hibernate annotations are removed
private Long id;
private String name;
private String value;
}
In MapStruct
we need to define an interface with all methods we need.
import com.example.demo.model.SampleCompleteRequest;
import com.example.demo.model.SamplePartialRequest;
import com.example.demo.model.SampleResponse;
import jakarta.annotation.Nullable;
import org.mapstruct.BeanMapping;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.ReportingPolicy;
import java.util.Optional;
import static org.mapstruct.MappingConstants.ComponentModel.SPRING;
import static org.mapstruct.NullValueCheckStrategy.ALWAYS;
import static org.mapstruct.NullValuePropertyMappingStrategy.IGNORE;
@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE, componentModel = SPRING)
public interface SamplesMapper {
@BeanMapping(nullValueCheckStrategy = ALWAYS, nullValuePropertyMappingStrategy = IGNORE)
Sample patch(SamplePartialRequest input, @MappingTarget Sample target);
Sample update(SampleCompleteRequest input, @MappingTarget Sample target);
SampleResponse mapToResponse(Sample input);
default String optionalToString(@Nullable Optional<String> nullable) {
return nullable == null ? null : nullable.orElse(null);
}
}
Plugin will generate boilerplate code for us. Below class is autogenerated and we do not need to implement it manually.
@Component
public class SamplesMapperImpl implements SamplesMapper {
@Override
public Sample patch(SamplePartialRequest input, Sample target) {
if ( input == null ) {
return target;
}
if ( input.getName() != null ) {
target.setName( optionalToString( input.getName() ) );
}
if ( input.getValue() != null ) {
target.setValue( optionalToString( input.getValue() ) );
}
return target;
}
@Override
public Sample update(SampleCompleteRequest input, Sample target) {
if ( input == null ) {
return target;
}
target.setName( input.getName() );
target.setValue( input.getValue() );
return target;
}
@Override
public SampleResponse mapToResponse(Sample input) {
if ( input == null ) {
return null;
}
SampleResponse sampleResponse = new SampleResponse();
sampleResponse.setId( input.getId() );
sampleResponse.setName( input.getName() );
sampleResponse.setValue( input.getValue() );
return sampleResponse;
}
}
A controller class is easy to implement:
import com.example.demo.model.SampleCompleteRequest;
import com.example.demo.model.SamplePartialRequest;
import com.example.demo.model.SampleResponse;
import com.example.service.SamplesMapper;
import com.example.service.SamplesService;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import org.springframework.hateoas.CollectionModel;
import org.springframework.hateoas.EntityModel;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@AllArgsConstructor
@RestController
@RequestMapping(value = "/api/v1/samples")
public class SamplesResource {
private final SamplesMapper mapper;
private final SamplesService samplesService;
@GetMapping
public CollectionModel<SampleResponse> listAll() {
List<SampleResponse> entities = samplesService.list().stream().map(mapper::mapToResponse).toList();
return CollectionModel.of(entities);
}
@PostMapping
public EntityModel<SampleResponse> addSample(@Valid @RequestBody SampleCompleteRequest request) {
var entity = samplesService.create(request);
var response = mapper.mapToResponse(entity);
return EntityModel.of(response);
}
@PutMapping(path = "{id}")
public EntityModel<SampleResponse> updateSample(@PathVariable Long id, @Valid @RequestBody SampleCompleteRequest request) {
var entity = samplesService.update(id, request);
var response = mapper.mapToResponse(entity);
return EntityModel.of(response);
}
@PatchMapping(path = "{id}")
public EntityModel<SampleResponse> partiallyUpdateSample(@PathVariable Long id, @Valid @RequestBody SamplePartialRequest request) {
var entity = samplesService.patch(id, request);
var response = mapper.mapToResponse(entity);
return EntityModel.of(response);
}
}
A service class is also straightforward:
import com.example.demo.model.SampleCompleteRequest;
import com.example.demo.model.SamplePartialRequest;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@AllArgsConstructor
public class SamplesService {
private final SamplesMapper mapper;
private final SamplesRepository repository;
public List<Sample> list() {
return repository.listAll();
}
public Sample create(SampleCompleteRequest request) {
var sample = mapper.update(request, new Sample());
return repository.save(sample);
}
public Sample update(Long id, SampleCompleteRequest request) {
var sample = repository.find(id).orElseThrow();
mapper.update(request, sample);
return repository.save(sample);
}
public Sample patch(Long id, SamplePartialRequest request) {
var sample = repository.find(id).orElseThrow();
mapper.patch(request, sample);
return repository.save(sample);
}
}
See also: