springboot error - Could not write JSON: Can not start an array, expecting field name (context: Object)
에러 발생
.HttpMessageNotWritableException:
Could not write JSON: Can not start an array, expecting field name (context: Object);
nested exception is com.fasterxml.jackson.databind.JsonMappingException:
Can not start an array, expecting field name (context: Object)
(through reference chain: org.springframework.hateoas.EntityModel["content"])]
위의 에러는 테스트 코드를 돌리는데 발생.
@Test
@DisplayName("입력 값이 잘못된 경우에 에러가 발생하는 테스트")
public void createEvent_Bad_Request_Wrong_Input() throws Exception {
EventDto eventDto = EventDto.builder()
.name("Springboot")
.description("REST api development with springboot")
.beginEnrollmentDateTime(LocalDateTime.of(2022,02,28,17,32))
.closeEnrollmentDateTime(LocalDateTime.of(2022,02,24,17,32))
.beginEventDateTime(LocalDateTime.of(2022,02,24,17,32))
.endEventDateTime(LocalDateTime.of(2022,02,23,17,32))
.basePrice(10000)
.maxPrice(200)
.limitOfEnrollment(100)
.location("제주 첨단로 카카오스페이스 닷 투")
.build();
mockMvc.perform(post("/api/events")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaTypes.HAL_JSON)
.content(objectMapper.writeValueAsString(eventDto)))
.andDo(print())
.andExpect(status().isBadRequest())
.andExpect(jsonPath("errors[0].objectName").exists())
.andExpect(jsonPath("errors[0].defaultMessage").exists())
.andExpect(jsonPath("errors[0].code").exists())
.andExpect(jsonPath("_links.index").exists())
;
}
Controller code
package kr.pe.acet.acetrestapi.events.controller;
import kr.pe.acet.acetrestapi.events.dto.Event;
import kr.pe.acet.acetrestapi.events.dto.EventDto;
import kr.pe.acet.acetrestapi.events.dto.EventResource;
import kr.pe.acet.acetrestapi.events.repository.EventRepository;
import kr.pe.acet.acetrestapi.events.EventValidator;
import kr.pe.acet.acetrestapi.utils.ErrorsResource;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.validation.Valid;
import java.net.URI;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
@Controller
@RequestMapping(value = "/api/events", produces = MediaTypes.HAL_JSON_VALUE+";charset=UTF-8")
public class EventController {
// @Autowired EventRepository eventRepository;
private final EventRepository eventRepository;
private final ModelMapper modelMapper;
private final EventValidator eventValidator;
@Autowired ErrorsResource errorsResource;
public EventController(EventRepository eventRepository, ModelMapper modelMapper, EventValidator eventValidator) {
this.eventRepository = eventRepository;
this.modelMapper = modelMapper;
this.eventValidator = eventValidator;
}
@PostMapping
public ResponseEntity<EntityModel> createEvent(@RequestBody @Valid EventDto eventDto, Errors errors){
if (errors.hasErrors()) {
return badRequest(errors);
}
eventValidator.validate(eventDto, errors);
if (errors.hasErrors()) {
return badRequest(errors);
}
Event event = modelMapper.map(eventDto, Event.class );
event.update();
Event eventSave = this.eventRepository.save(event);
WebMvcLinkBuilder selfLinkbuilder = linkTo(EventController.class).slash(eventSave.getId());
URI createdUri = selfLinkbuilder.toUri();
EventResource eventResource = new EventResource(event);
eventResource.add(linkTo(EventController.class).withRel("query-events"));
eventResource.add(selfLinkbuilder.withRel("update-event"));
eventResource.add(Link.of("/docs/index.html#resources-events-create", "profile"));
return ResponseEntity.created(createdUri).body(eventResource);
}
private ResponseEntity<EntityModel> badRequest(Errors errors) {
return ResponseEntity.badRequest().body(errorsResource.addLink(errors));
}
}
오류 체크를 하는 테스트이며 아래의 소스를 수정하는 과정에서 오류발생.
if (errors.hasErrors()) {
return badRequest(errors);
}
eventValidator.validate(eventDto, errors);
if (errors.hasErrors()) {
return badRequest(errors);
}
private ResponseEntity<EntityModel> badRequest(Errors errors) {
return ResponseEntity.badRequest().body(errorsResource.addLink(errors));
}
ErrorsResource.java
처음에는 아래처럼 ErrorsResource extends EntityModel 을 상속 받아서 생성자로 content, link를 add 시킴.
public class ErrorsResource extends EntityModel {
public ErrorsResource(Errors content, Link... links){
super(content, Arrays.asList(links));
add(linkTo(methodOn(IndexController.class).indxe()).withRel("index"));
}
}
오류 발생 : 생성자에서 문제 발생 (protected로 되어있음 - 같은 패키지 아니면 접근 X)
Resolved Exception: Type = org.springframework.http.converter.HttpMessageNotWritableException
그래서 아래와 같이 EntityModel.of를 이용하여 content와 link를 추가.
@Component
public class ErrorsResource{
public EntityModel addLink(Errors content) {
EntityModel<Errors> entityModel = EntityModel.of(content);
entityModel.add(linkTo(methodOn(IndexController.class).indxe()).withRel("index"));
return entityModel;
}
}
디버깅 해봤을 때 data 잘들어오는것을 확인. 그런데 오류 발생.
.HttpMessageNotWritableException: Could not write JSON: Can not start an array, expecting field name
(context: Object); nested exception is com.fasterxml.jackson.databind.JsonMappingException:
Can not start an array, expecting field name (context: Object)
(through reference chain: org.springframework.hateoas.EntityModel["content"])]
찾아봤더니 Springboot 2.3이상부터 Jackson lib가 Array부터 만드는걸 허용하지 않는다고 함.
gen.writeFieldName("errors");
gen.writeStartArray();
를 추가해서 해결 함 .
https://www.inflearn.com/questions/72123
package kr.pe.acet.acetrestapi.utils;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.springframework.boot.jackson.JsonComponent;
import org.springframework.validation.Errors;
import java.io.IOException;
@JsonComponent
public class ErrorsSerializer extends JsonSerializer<Errors> {
@Override
public void serialize(Errors value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeFieldName("errors");
gen.writeStartArray();
value.getFieldErrors().forEach(e->{
try{
gen.writeStartObject();
gen.writeStringField("field", e.getField());
gen.writeStringField("objectName", e.getObjectName());
gen.writeStringField("code", e.getCode());
gen.writeStringField("defaultMessage", e.getDefaultMessage());
Object rejectValue = e.getRejectedValue();
if (rejectValue != null){
gen.writeStringField("rejectedValue", rejectValue.toString());
}
gen.writeEndObject();
} catch (IOException e1) {
e1.printStackTrace();
}
});
value.getGlobalErrors().forEach(e -> {
try{
gen.writeStartObject();
gen.writeStringField("objectName", e.getObjectName());
gen.writeStringField("code", e.getCode());
gen.writeStringField("defaultMessage", e.getDefaultMessage());
gen.writeEndObject();
} catch (IOException e1) {
e1.printStackTrace();
}
});
gen.writeEndArray();
}
}