springboot error - Could not write JSON: Can not start an array, expecting field name (context: Object)

2022. 3. 25. 15:58OpenSource/Spring Boot

반응형

에러 발생

.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();
    }
}
반응형