Spring boot cannot find the data sent in request body.
As specified below, in code extracts, I send form with application/x-www-form-urlencoded
content-type to the endpoint POST /cards
.
The good method is called by Spring boot but data from the request body aren't loaded in card entity, which is passed as parameter (see console output below).
Versions:
2020-10-21 00:26:58.594 DEBUG 38768 --- [nio-8080-exec-1] c.b.c.c.f.RequestResponseLoggingFilter : New request method=POST path=/cards content-type=application/x-www-form-urlencoded
2020-10-21 00:26:58.595 DEBUG 38768 --- [nio-8080-exec-1] c.b.c.c.f.RequestResponseLoggingFilter : RequestBody: title=First+card&seoCode=first-card&description=This+is+the+first+card+of+the+blog&content=I+think+I+need+help+about+this+one...
### createNewCard ###
card: Card<com.brunierterry.cards.models.Card@34e63b41>{id=null, seoCode='null', publishedDate=null, title='null', description='null', content='null'}
result: org.springframework.validation.BeanPropertyBindingResult: 0 errors
model: {card=Card<com.brunierterry.cards.models.Card@34e63b41>{id=null, seoCode='null', publishedDate=null, title='null', description='null', content='null'}, org.springframework.validation.BindingResult.card=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
2020-10-21 00:26:58.790 TRACE 38768 --- [nio-8080-exec-1] c.b.c.c.f.RequestResponseLoggingFilter : Response to request method=POST path=/cards status=200 elapsedTime=196ms
(Here I read body with req.getReader()
, but I comment it usually to not consume the buffer.)
@Controller
public class CardController implements ControllerHelper {
@PostMapping(value = "/cards", consumes = MediaType.ALL_VALUE)
public String createNewCard(
@ModelAttribute Card card,
BindingResult result,
ModelMap model
) {
System.out.println("\n### createNewCard ###\n");
System.out.println("card: "+card);
System.out.println("result: "+result);
System.out.println("model: "+model);
return "/cards/editor";
}
@GetMapping(value = "/cards/form")
public String newPost(
Model model
) {
model.addAttribute("card", Card.defaultEmptyCard);
return "/cards/editor";
}
}
<form action="/cards"
method="POST"
modelAttribute="card"
enctype="application/x-www-form-urlencoded"
>
<div class="form-group">
<label for="title">Title & SEO slug code</label>
<div class="form-row">
<div class="col-9">
<@spring.formInput
"card.title"
"class='form-control' placeholder='Title'"
/>
<@spring.showErrors "<br>"/>
</div>
<div class="col-2">
<@spring.formInput
"card.seoCode"
"class='form-control' placeholder='SEO slug code' aria-describedby='seoCodeHelp'"
/>
<@spring.showErrors "<br>"/>
</div>
<div class="col-1">
<@spring.formInput
"card.id"
"DISABLED class='form-control' placeholder='ID'"
/>
</div>
</div>
<div class="form-row">
<small id="seoCodeHelp" class="form-text text-muted">
Keep SEO slug very small and remove useless words.
</small>
</div>
</div>
<div class="form-group">
<label for="description">Description</label>
<@spring.formInput
"card.description"
"class='form-control' placeholder='Short description of this card..' aria-describedby='descriptionHelp'"
/>
<small id="descriptionHelp" class="form-text text-muted">
Keep this description as small as possible.
</small>
</div>
<div class="form-group">
<label for="content">Content</label>
<@spring.formTextarea
"card.content"
"class='form-control' rows='5'"
/>
</div>
<button type="submit" class="btn btn-primary">Save</button>
</form>
@Entity
public class Card implements Comparable<Card> {
protected Card() {}
public static final Card defaultEmptyCard = new Card();
private final static Logger logger = LoggerFactory.getLogger(Card.class);
@Autowired
private ObjectMapper objectMapper;
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
@NotBlank(message = "Value for seoCode (the slug) is mandatory")
@Column(unique=true)
private String seoCode;
@JsonDeserialize(using = LocalDateDeserializer.class)
@JsonSerialize(using = LocalDateSerializer.class)
private LocalDate publishedDate;
@NotBlank(message = "Value for title is mandatory")
private String title;
@NotBlank(message = "Value for description is mandatory")
private String description;
@NotBlank(message = "Value for content is mandatory")
private String content;
public boolean hasIdUndefine() {
return null == id;
}
public boolean hasIdDefined() {
return null != id;
}
public Long getId() {
return id;
}
public String getSeoCode() {
return seoCode;
}
public LocalDate getPublishedDate() {
return publishedDate;
}
public String getTitle() {
return title;
}
public String getDescription() {
return description;
}
public String getContent() {
return content;
}
private String formatSeoCode(String candidateSeoCode) {
return candidateSeoCode.replaceAll("[^0-9a-zA-Z_-]","");
}
private Card(
@NonNull String rawSeoCode,
@NonNull String title,
@NonNull String description,
@NonNull String content,
@NonNull LocalDate publishedDate
) {
this.seoCode = formatSeoCode(rawSeoCode);
this.title = title;
this.description = description;
this.content = content;
this.publishedDate = publishedDate;
}
public static Card createCard(
@NonNull String seoCode,
@NonNull String title,
@NonNull String description,
@NonNull String content,
@NonNull LocalDate publishedDate
) {
return new Card(
seoCode,
title,
description,
content,
publishedDate
);
}
public static Card createCard(
@NonNull String seoCode,
@NonNull String title,
@NonNull String description,
@NonNull String content
) {
LocalDate publishedDate = LocalDate.now();
return new Card(
seoCode,
title,
description,
content,
publishedDate
);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Card card = (Card) o;
return Objects.equals(id, card.id) &&
seoCode.equals(card.seoCode) &&
publishedDate.equals(card.publishedDate) &&
title.equals(card.title) &&
description.equals(card.description) &&
content.equals(card.content);
}
@Override
public int hashCode() {
return Objects.hash(id, seoCode, publishedDate, title, description, content);
}
@Override
public String toString() {
return "Card<"+ super.toString() +">{" +
"id=" + id +
", seoCode='" + seoCode + '\'' +
", publishedDate=" + publishedDate +
", title='" + title + '\'' +
", description='" + description + '\'' +
", content='" + content + '\'' +
'}';
}
public Either<JsonProcessingException,String> safeJsonSerialize(
ObjectMapper objectMapper
) {
try {
return Right(objectMapper.writeValueAsString(this));
} catch (JsonProcessingException e) {
logger.error(e.getMessage());
return Left(e);
}
}
public Either<JsonProcessingException,String> safeJsonSerialize() {
try {
return Right(objectMapper.writeValueAsString(this));
} catch (JsonProcessingException e) {
logger.error(e.getMessage());
return Left(e);
}
}
@Override
public int compareTo(@NotNull Card o) {
int publicationOrder = this.publishedDate.compareTo(o.publishedDate);
int defaultOrder = this.seoCode.compareTo(o.seoCode);
return publicationOrder == 0 ? defaultOrder : publicationOrder;
}
}
I got a good answer. It works when adding empty constructor and setters to the Card entity. However, it's not the class I want. I want card to be only instantiated with a constructor that have all parameters. Do you have an idea about how to achieve that ? Should I create another class to represent the form ? Oris there a way to only allow Spring to use such setters ?
Did you make sure that you Card.java
has the appropriate getters and setters? This way spring can actually populate the data in the object it is trying to create.