I'm implementing query layer on database by using GraphQl and spring boot project to perform CRUD operation on sql database. In GraphQL schema i mentioned some fields to be mandatory and when those fields are not mentioned in query it is returning ValidationError error message in default format with 200 status code.
Error :
{
"data": null,
"errors": [
{
value=StringValue{value='1235'}}]}}]}' is missing required fields '[book_type]' # 'create_book'",
"locations": [
{
"line": 3,
"column": 23,
"sourceName": null
}
],
"description": "argument 'insert' with value value=StringValue{value='1235'}}]}}]}' is missing required fields '[book_type]'",
"validationErrorType": "WrongType",
"queryPath": [
"create_book"
],
"errorType": "ValidationError",
"path": null,
"extensions": null
}
],
"dataPresent": false,
"extensions": null
}
And here is my code with layer architecture pattern
Controller :
#Autowired
private GraphQLServer graphQlServer;
#PostMapping("test")
public ResponseEntity<Object> graphQl(#RequestBody String body){
ExecutionResult response = graphQlServer.execute(body);
return ResponseEntity.ok(response);
}
Service :
#Service
public class GraphQLServer {
#Autowired
private GraphQL graphQl;
public ExecutionResult execute(String query) {
return graphQl.execute(query);
}
}
Config :
#Bean
public GraphQL loadSchema() throws IOException {
File schemaFile = schemaResource.getFile();
TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(schemaFile);
RuntimeWiring wiring = buildRuntimeWiring();
GraphQLSchema schema = new SchemaGenerator().makeExecutableSchema(typeRegistry, wiring);
return GraphQL.newGraphQL(schema).build();
}
private RuntimeWiring buildRuntimeWiring() {
return RuntimeWiring.newRuntimeWiring()
.type("Mutation", mutationWiring -> mutationWiring.dataFetcher("create_book", bookDataFetcher))
.build();
}
BookDataFetcher :
#Override
public Map<String, Object> get(DataFetchingEnvironment environment) {
//return data from db by getting Map properties from environment
}
The above code is working as expected but my question here is How to customize the error message? In the error message i would like to mention the status 400 since it is bad request from client
First of all , you should call toSpecification() on ExecutionResult to make sure the response obeys the GraphQL Specification.
By default , there is only one ExecutionResult 's implementation provided by graphql-java which is ExecutionResultImpl , so you can cast ExecutionResult to it in order to use its transform() to update its state.
ExecutionResultImpl internally contains all errors detected by the graphql-java. All of them are in the subclass of GraphQLError which mean you have to cast it to the specific sub-class during customization.
In your case , the subclass is ValidationError and the codes look something like :
#PostMapping("test")
public ResponseEntity<Object> graphQl(#RequestBody String body){
ExecutionResult response = graphQlServer.execute(body);
ExecutionResultImpl responseImpl = (ExecutionResultImpl) response;
List<GraphQLError> customizedErrors = Lists.newArrayList();
for (GraphQLError gqlError : responseImpl.getErrors()) {
//Do your error custmosation here....
GraphQLError customizedError = gqlError;
if (gqlError instanceof ValidationError) {
ValidationError error = (ValidationError) gqlError;
customizedError = new ValidationError(error.getValidationErrorType(), error.getLocations(),
"Customizing some error message blablabla....");
}
customizedErrors.add(customizedError);
}
Map<String, Object> specResponse = responseImpl.transform(b->b.errors(customizedErrors)).toSpecification();
return ResponseEntity.ok(specResponse);
}
Related
I'm a newbie with Spring Boot, and I need your help.
I make a GET request with WebClient, and I receive a JSON body as below:
{
"status": "OK",
"error": [],
"payload": {
"name": "John",
"surname": "Doe"
...
}
}
So I have a DTO class in which mapping the response. Something like this:
#Data
#AllArgsConstructor
#NoArgsConstructor
public class ResponseAccountDTO {
private String status;
private List<ErrorDTO> errors;
private User payload;
}
I do it whit this method:
public ResponseUserDTO retrieveUserById(String userId) {
return webClient.get()
.uri(GET_USER_BY_ID_V4, accountId)
.header("Auth-Schema", AUTH_SCHEMA)
.header("apikey", API_KEY)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatus::is4xxClientError, response -> {
System.out.println("4xx error");
return Mono.error(new RuntimeException("4xx"));
})
.onStatus(HttpStatus::is5xxServerError, response -> {
System.out.println("5xx error");
return Mono.error(new RuntimeException("5xx"));
})
.bodyToMono(ResponseDTO.class)
.block();
}
Finally, I test it with this method:
UserRestClient userRestClient = new UserRestClient(webClient);
#Test
void retrieveUser() {
ResponseDTO response = userRestClient.retrieveUserById("123");
UserDTO user = response.getPayload();
System.out.println("user surname: " + user.surname);
assertEquals("Doe", user.getSurname());
}
All fine until the response has KO Status. If something goes wrong (i.e., BAD REQUEST), I receive the same body JSON structure, as below:
{
"status": "KO",
"errors": [
{
"code": "ER000",
"description": "Wrong ID parameter",
"params": ""
}
],
"payload": {}
}
Is there a way to map also with KO Status the JSON body on my DTO class?
I want to return the error description on my retrieveUser() method.
Update:
I add my ErrorDTO class as suggest by Seelenvirtuose
#Data
#AllArgsConstructor
#NoArgsConstructor
public class ErrorDTO {
private String code;
private String description;
private String params;
}
I myself ran into this issue and had to convert the json error response to an ErrorDTO object.
Hope the below code helps you for what you are looking for.
The below code can be applied to any Status code (e.g. 4xx, 5xx and even for 2xx as well but you won't need it for 2xx)
.onStatus(HttpStatus::is4xxClientError, error -> error
.bodyToMono(Map.class)
.flatMap(body -> {
try {
var message = objectMapper.writeValueAsString(body);
ErrorDTO errorResponse = objectMapper.readValue(message, ErrorDTO.class);
return Mono.error(new ServiceException(error.statusCode().value(), "My custom error message", errorResponse));
} catch (JsonProcessingException jsonProcessingException) {
return Mono.error(new ServiceException("Cannot parse the error response"));
}
})
)
I'm receiving JSON from REST API looks like:
{
"items": [
{
"id": 60659,
"name": "Display",
"active": true,
"account_id": 235
},
{
"id": 36397,
"name": " Mail Display",
"active": true,
"account_id": 107
}
]
}
I'm using this method to parse it:
Mono<List<Item>> getItems(String token) {
return webCLient
.get()
.headers(httpHeaders -> httpHeaders.setBearerAuth(token))
.retrieve()
.bodyToMono(ItemResponse.class)
.map(ItemResponse::getResponse)
.retryBackoff(RetrySettings.RETRIES, RetrySettings.FIRST_BACKOFF, RetrySettings.MAX_BACKOFF)
.doOnError(e -> log.error("error: " + e.getCause().toString()))
Response:
public class ItemResponse {
#JsonProperty("items")
private List<Item> response;
}
But sometimes 3rd party API returns different response without top level items property and looks like:
[
{
"id": 60659,
"name": "Display",
"active": true,
"account_id": 235
},
{
"id": 36397,
"name": " Mail Display",
"active": true,
"account_id": 107
}
]
At this point my app is crashing with JSON decoding error. I used for this case:
bodyToMono(new ParameterizedTypeReference<List<Item>>() {})
But I can't always refactoring this part of code just to handle their json. How to do it in dynamical way with Spring WebFlux? Like try -> parse#1 -> catch -> parse#2. So i need to parse json in way#1 and if error occurs app should try to parse it with way#2.
You can get the response as a string .bodyToMono(String.class) and do whatever you want, with multiple try catches... but I think your best bet is to create a custom Deserializer and use it with your WebClient via ExchangeStrategies like described here: How to customize SpringWebFlux WebClient JSON deserialization?
.
class MyResponse {
List<Object> data;
MyResponse(List<Object> data) {
this.data = data;
}
}
class MyResponseDeserializer extends JsonDeserializer<MyResponse> {
#Override
public MyResponse deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
TreeNode treeNode = jsonParser.getCodec().readTree(jsonParser);
List<Object> data = new ArrayList<>();
if (treeNode.isArray()) {
// parse it as array
} else {
// parse it as object and put inside list
}
MyResponse myResponse = new MyResponse(data);
return myResponse;
}
}
And then
WebClient getWebClient() {
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule();
simpleModule.addDeserializer(MyResponse.class, new MyResponseDeserializer());
objectMapper.registerModule(simpleModule);
ExchangeStrategies strategies = ExchangeStrategies
.builder()
.codecs(clientDefaultCodecsConfigurer -> {
clientDefaultCodecsConfigurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(objectMapper, MediaType.APPLICATION_JSON));
clientDefaultCodecsConfigurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(objectMapper, MediaType.APPLICATION_JSON));
}).build();
return WebClient.builder().exchangeStrategies(strategies).build();
}
Mono<List<Item>> getItems(String token) {
return getWebClient()
.get()
.headers(httpHeaders -> httpHeaders.setBearerAuth(token))
.retrieve()
.bodyToMono(MyResponse.class)
.map(MyResponse::data)
.retryBackoff(RetrySettings.RETRIES, RetrySettings.FIRST_BACKOFF, RetrySettings.MAX_BACKOFF)
.doOnError(e -> log.error("error: " + e.getCause().toString()))
}
The rest is the same as in your example just change the class name and add appropriate fields.
And of course this is just a fast written demo and everything hardcoded and within a one method, better to have them injected
I am getting below response when I am calling an API.
Response postRequestResponse = ConnectionUtil.getwebTarget()
.property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true)
.path("bots")
.path(ReadSkillID.readSkillId())
.path("dynamicEntities").path(dynamicEntityID)
.path("pushRequests").path(pushRequestID).path(operation)
.request()
.header("Authorization", "Bearer " + ConnectionUtil.getToken())
.get();
Below output I am getting.
{
"createdOn": "2020-08-17T12:19:13.541Z",
"updatedOn": "2020-08-17T12:19:23.421Z",
"id": "C84B058A-C8F9-41F5-A353-EC2CFE7A1BD9",
"status": "TRAINING",
"statusMessage": "Request Pushed into training, on user request"
}
I have to return this output to client with an additional field in the response. How can modify the above response and make it
{
"EntityName": "NewEntity", //New field
"createdOn": "2020-08-17T12:19:13.541Z",
"updatedOn": "2020-08-17T12:19:23.421Z",
"id": "C84B058A-C8F9-41F5-A353-EC2CFE7A1BD9",
"status": "TRAINING",
"statusMessage": "Request Pushed into training, on user request"
}
I am adding this additional field here
"EntityName": "NewEntity"
How can I do that. many things I tried but got exception.
get JSON from postRequestResponse (i have no idea what framework you are using, so you have to figer it out on your own, but the Response datatype will probably have a getResponseBody or similar method returing the JSON)
add EntityName
serialize it again to json.
class YourBean {
#Autowired
private ObjectMapper objectMapper;
public void yourMethod() {
// 1
final InputStream jsonFromResponse = ...
// 2
Map dataFromResponse = objectMapper.readValue(jsonFromResponse, Map.class);
dataFromResponse.put("EntityName", "NewEntity");
// 3
final String enrichedJson = objectMapper.writeValueAsString(dataFromResponse);
}
}
enrichedJson contains EntityName and whatever comes from the API.
Is it possible to return a validation annotations message in the response for bad responses? I thought this was possible but I have noticed that our projects are not given detailed bad request messages.
#NotNull(message="idField is required")
#Size(min = 1, max = 15)
private String idField;
I'd like to see "idField is required" returned if a request is made that's missing the idField. I am using jersey 2.0. What I'm seeing for a response is this...
{
"timestamp": 1490216419752,
"status": 400,
"error": "Bad Request",
"message": "Bad Request",
"path": "/api/test"
}
It looks like your Bean validation exception(ConstraintViolationException) is translated by one of your ExceptionMappers. You can register an ExceptionMapper for ConstraintViolationException as shown below and return data in the format you want. ConstraintViolationException has all the information you are looking for.
#Singleton
#Provider
public class ConstraintViolationMapper implements ExceptionMapper<ConstraintViolationException> {
#Override
public Response toResponse(ConstraintViolationException e) {
// There can be multiple constraint Violations
Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
List<String> messages = new ArrayList<>();
for (ConstraintViolation<?> violation : violations) {
messages.add(violation.getMessage()); // this is the message you are actually looking for
}
return Response.status(Status.BAD_REQUEST).entity(messages).build();
}
}
Building upon Justin response, here it is a version that uses Java 8 stream API:
#Singleton
#Provider
public class ConstraintViolationMapper implements ExceptionMapper<ConstraintViolationException> {
#Override
public Response toResponse(ConstraintViolationException e) {
List<String> messages = e.getConstraintViolations().stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.toList());
return Response.status(Status.BAD_REQUEST).entity(messages).build();
}
}
I'm facing a tricky behavior from my REST resource.
The exposed method is expecting a complex json object :
#Path(RestURIConstant.NOTIFICATION_ROOT_URI)
#Component
#Scope("request")
public class NotificationResource implements RestURIConstant {
/** Notification service. */
#Autowired
private INotificationService notificationService;
#Path(RestURIConstant.COMPLEMENT_URI)
#POST
#Consumes(MediaType.APPLICATION_JSON)
#Produces(MediaType.APPLICATION_JSON)
public Response processNotification(final EventDTO event)
throws BusinessException, TechnicalException {
checkParameters(event);
notificationService.processEvent(event);
return Response.ok().build();
}
}
EventDTO has two enum fields : notificationType and eventType :
public class EventDTO {
private ENotificationType notificationType;
private EEventType eventType;
private String eventDate;
private String userName;
//... getters, setters
}
What I want is to map exception from any kind of data validation error to get at the end a json response with an error code and error message. And after following jax-rs jersey: Exception Mapping for Enum bound FormParam :
So for the ENoticationType I wrote :
public enum ENotificationEventType {
RULE,
ALARM,
ACK,
INFO;
#JsonCreator
public static ENotificationEventType fromString(final String typeCode)
throws BusinessException {
if (typeCode == null) {
throw new BusinessException(ValidationCode.VALUE_REQUIRED, "type");
}
try {
return valueOf(typeCode);
} catch (final IllegalArgumentException iae) {
throw new BusinessException(ValidationCode.UNSUPPORTED_VALUE, "type", typeCode, Arrays.toString(values()));
}
}
}
And for the Mapper I wrote :
#Provider
#Singleton
public class BusinessExceptionMapper implements ExceptionMapper<BusinessException> {
#Override
public Response toResponse(final BusinessException exception) {
Status status = Status.INTERNAL_SERVER_ERROR;
// If a validationCode error = unsupported version => CODE 410
if (exception.getErrorCode().equals(ValidationCode.UNSUPPORTED_API_VERSION)) {
status = Status.GONE;
} else if (exception.getErrorCode().getClass().isAssignableFrom(ValidationCode.class)) {
// If a validationCode error then BAD_REQUEST (400) HTTP
status = Status.BAD_REQUEST;
} else if (exception.getErrorCode().getClass().isAssignableFrom(NotFoundCode.class)) { // CODE 404
status = Status.NOT_FOUND;
} else if (exception.getErrorCode().getClass().isAssignableFrom(SecurityCode.class)) { // CODE 401
status = Status.UNAUTHORIZED;
} else if (exception.getErrorCode().getClass().isAssignableFrom(AdminSecurityCode.class)) { // CODE 401
status = Status.UNAUTHORIZED;
}
return Response.status(status).type(MediaType.APPLICATION_JSON)
.entity(ErrorMessageHelper.createErrorMessageHelper(
exception.getErrorCode(), exception.getMessage()))
.build();
}
And my application-context contains <context:component-scan base-package=" com.technicolor.hovis.backend.rest, com.technicolor.hovis.admin.rest" />
I already read several answers to questions relative to Exception mapping in jersey but in my case, it's not that the mapping is not recognized but that it's not applied in all cases :
the exceptions thrown by checkParameters are mapped and the result is as expected
but if an invalid enum is sent, the #JsonCreator method is called, throw the same type of exception but this one is not mapped as expected.
So The response looks like :
<data contentType="text/plain;charset=UTF-8" contentLength="176">
<![CDATA[Unsupported type value : 'ALARN'. Expected values are [RULE, ALARM, ACK, INFO] (through reference chain: EventDTO["type"])]]>
</data>
And not the expected :
{
"code": 6,
"message": "Unsupported type value : 'ALARN'. Expected values are [RULE, ALARM, ACK, INFO]"
}
Any idea ?
Cyril
I would try changing the BusinessExceptionMapper to:
public class BusinessExceptionMapper implements ExceptionMapper<Exception>
If I understand correctly, the problem is when deserializng the parameter you except, in that case an IOException will be thrown and not BusinessException which I am guessing is your custom notification. So you can either extend ExceptionMapper<Exception> or ExceptionMapper<IOException>
I noticed that when I throw a checked exception in JSONCreator (just like you did), Jackson catches it and wraps it in IllegalArgumentException, so IllegalArgumentException ends up in ExceptionMapper.
Maybe this was cause of your problems?