spring-hateoas
spring-hateoas copied to clipboard
Support for HAL-FORMS value element
Fixes #1717
Behaviour before this change
Let be:
class MyController {
@GetMapping
public ResponseEntity<?> list() {
Link selfLink = selfLink.andAffordance(afford(methodOn(MyController.class).create(null)));
return ResponseEntity.ok(new RepresentationModel<>(selfLink));
}
@PostMapping
public ResponseEntity<?> create(@RequestBody Payload payload) {
return ResponseEntity.created().build();
}
}
record Payload(String foo, String bar) {
Payload {
// 'foo' and 'bar' are mandatory
// we verify this invariant in the constructor to automatically end up with an HTTP 400 bad request in case of thrown exception
requireNonNull(foo);
requireNonNull(bar);
}
}
Calling GET will return something like:
{
"_templates": {
"default": {
"properties": [
{
"name": "foo"
},
{
"name": "bar"
}
]
}
}
}
We want to be able to assign a default value hello to attribute foo in the HAL-FORMS payload to obtain something like this:
{
"_templates": {
"default": {
"properties": [
{
"name": "foo",
"value": "hello"
},
{
"name": "bar"
}
]
}
}
}
Solution brought by this change
The implemented solution is heavily inspired from HalFormsOptionsFactory. A consumer can provide a value creator, taking a property metadata as input and returning a value of type String as output.
Considered alternatives
Retrieve the value directly from the payload instance
To do that, this kind of consumer code would be needed:
@GetMapping
public ResponseEntity<?> list() {
Link selfLink = selfLink.andAffordance(afford(methodOn(MyController.class).create(new Payload("hello", null))));
return ResponseEntity.ok(new RepresentationModel<>(selfLink));
}
- this will not work since
baris verified for non-nullity inPayloadconstructor. - consumers will not either accept to relax the constructor invariant validation
On the 2nd point, some may argue that jakarta.validation.constraints.NotNull should be used instead of the in-house constructor validation. On that my opinion is as follow:
@NotNullshould be avoided when plain simple java code is able to enforce the same constraint- IDEs will probably at least emit a warning when seeing a
nullvalue assigned to a@NotNullproperty, at worst will fail the compilation
Retrieve the value from a new annotation on the considered property
This would force the consumer to know the value before compilation.
Regarding the first considered alternative (Retrieve the value directly from the payload instance), maybe using
jakarta.validation.constraints.NotNull with a validation group could be an acceptable trade-off that won't trigger the IDE?
public class Payload {
// constructor, getters, setters
@NotNull(groups = Submitted.class)
private String foo;
@NotNull(groups = Submitted.class)
private String bar;
}
public class MyController {
@GetMapping
public ResponseEntity<?> list() {
Link selfLink = selfLink.andAffordance(afford(methodOn(MyController.class).create(new Payload("hello", null))));
return ResponseEntity.ok(new RepresentationModel<>(selfLink));
}
@PostMapping
public ResponseEntity<?> create(@RequestBody @Validated(Submitted.class) Payload payload) {
return ResponseEntity.created().build();
}
}
@kalgon , when I look at this, I see an annotation hell ^^