Quarkus/JAX-RS: possible to get better data binding errors? - validation

I am currently working on a Quarkus application using JSON-B and I am doing something like the following:
public class Something {
public enum Choice { X, Y, Z }
private Choice a;
private Choice b;
private Choice c;
// getters and setters omitted
}
#Path("/test")
#Consumes(MediaType.APPLICATION_JSON)
#Produces(MediaType.APPLICATION_JSON)
public class TestResource {
#POST
public Response post(#Valid Something something) {
return Response.ok().build();
}
}
The following request results correctly in an exception:
POST http://localhost:8080/test
Content-Type: application/json
Accept: application/json
{
"a": "X",
"b": "TEST",
"c": "TEST2"
}
javax.ws.rs.ProcessingException: RESTEASY008200: JSON Binding deserialization error: javax.json.bind.JsonbException: Internal error: No enum constant com.example.Something.Choice.TEST
The problem with this is, that the JsonbException is thrown right when the first datatype conversion fails. From a validation and response standpoint I don't like this very much, as I would rather know EVERY attribute that is invalid (b and c).
Is there a way to make JSON-B deserialization report multiple errors?

Unfortunately no, there is no way to get a better error message.

Related

ResponseEntity<MyDTO> is Returning Missing/Incomplete JSON

I'm seeing an issue with JSON cut-off/missing/incomplete when getting a response back from my service in Spring Boot.
Example:
ResponseEntity<MyDTO> responseEntity = myService.getMyDTO();
return responseEntity;
public class MyService {
public ResponseEntity<MyDTO> getMyDTO() {
return restTemplate.exchange(requestUrl, HttpMethod.GET, new HttpEntity<>(httpHeaders), MyDTO.class)
}
}
When debugging and inspecting the body of ResponseEntity, which is MyDTO instance, it contains
all the expected fields.
public class MyDTO {
private Information information;
private Address address;
}
public class Information {
private String firstName;
private String lastName;
}
public class Address {
private String streetName;
}
Debugging:
MyDTO
body
information
firstName > "John"
lastName > "Doe"
address > null
Expected JSON:
{
"information": {
"firstName": "John",
"lastName": "Doe"
},
"address: null
}
Actual JSON:
{
"information": {
"firstName": "Jo
Yes, the ending brackets are even missing from the response JSON.
Note: address is null because the response from upstream service returns a 400 and we map that response to our DTO (MyDTO). Initially, I thought this was the problem until debugging confirmed that the mapping was done correctly.
Here's what's really strange. If I take that ResponseEntity's body and put it in another ResponseEntity, than it returns fine and works. The service even returns faster, which is weird too.
ResponseEntity responseEntity = myService.getMyDTO();
return new ResponseEntity(responseEntity.getBody(), responseEntity.getStatusCode());
Anyone knows what's going on? Is it a networking, Spring Boot, or my code issue? Why would returning a new ResponseEntity fix the issue?
Found the issue. The upstream service contains a 'Content-Length' header, which is too small and causing the missing/incomplete JSON on client-side.
This is happening because you are calling the restTemplate.exchange() method; if you use the getForObject() method (in case of post call postForObject()) instead it will work.
This method will first create the MyDTO class object and then you need to add that object in the ResponseEntity object to return it.
Example:
MyDTO myDtoObject = restTemplate.getForObject(requestUrl, MyDTO.class);
return new ResponseEntity<>(myDtoObject, HttpStatus.OK);

Why does RestTemplate returns ArrayList<LinkedHashMap> instead of real list of model type?

It is a Springboot project. The code snip is as below. At line 59, the desired retrun type for restTemplate.getForEntity is List<Template>. While debugging, I find that the actual return type is an ArrayList contains many LinkedHashMap.
While LinkedHashMap is not sub class of Template. I don't know why the expect result type and the actual result type match.
Could anyone tell why it doesn't return ArrayList<Template>, instead of ArrayList<LinkedHashMap>? Thanks.
Template is an model defined in our project.
public class Template {
private String id;
private String name;
private String content;
xxx getters and setters
}
And it is a controller where the resttemplate is invoked.
#PostMapping(value = "/getTemplatesByGroup", produces = "application/json;charset=UTF-8")
#ResponseBody
public EUDataGrid<Template> getTemplatesByGroup(#RequestParam(defaultValue = "-1") Integer groupId) {
EUDataGrid<Template> grid = new EUDataGrid<>();
xxxx
List<Template> list = restTemplate.getForEntity(urlFullTemplates, ArrayList.class).getBody();
xxxx
return grid;
}
Json result format as below
[
{
"id": 1788,
"name": "xxxx",
"content": "xxxxx."
},
{
"id": 1787,
"name": "xxxxx",
"content": "xxxx"
}
]
Edit:
I googled a lot for this issue again. It is a common problem. There are similar scenarios some guys also encountered. I add the link in the foot of this post.
It seems that this is a bug of RestTemplate to handle generic properly. And there are ways to resolve this.
Here I want to know, why it doesn't throw exception when restTemplate returns ArrayList<LinkedHashMap> and assign it to List<Template>? They are differnt types. This is some kind of like assgin an int to a string.
I guess there is some magic with generic type. Could someone tell more about this? Thanks.
Unable to get a generic ResponseEntity<T> where T is a generic class "SomeClass<SomeGenericType>"
Using Spring RestTemplate in generic method with generic parameter
RestTemplate: how to get generic List response
Here is the problem in JSON response you are getting List<Template> or Array of Template, but in the responseType you just specified ArrayList where jackson doesn't know which type of ArrayList it is
limitation See the limitation
getForEntity(URI url, Class<T> responseType)
This sends a request to the specified URI using the GET verb and converts the response body into the requested Java type. This works great for most classes, but it has a limitation: we cannot get lists of objects.
One way is simple just specify Array type
Template[] list = restTemplate.getForEntity(urlFullTemplates, Template[].class).getBody();
Or use the exchange method

How to deserialize WWW form field to enum in Spring?

I'm facing issue when trying deserialize input coming from WWW form to enum class in Spring application, in Kotlin.
My DTO and enum classes:
enum class Status(#get:JsonValue val value: Int) {
NORMAL(0),
ERROR(1);
companion object {
#JvmStatic
#JsonCreator
fun of(number: Int?): Status? {
return values().find { it.value == number }
}
}
}
data class RequestData(val status: Status?)
Controller's POST request receiver method:
#PostMapping("/post")
fun register(#Valid data: RequestData, error: Errors) {}
When I make POST request with status = 0 using Postman, request's failing with following exception.
java.lang.IllegalArgumentException: Parameter specified as non-null is null: method com.example.Controller.post, parameter data
When I make request with status = NORMAL then no exception, but that what I don't want.
I'm using application/x-www-form-urlencoded content type in POST request.
Please let me know where I'm doing wrong.
You may use Converter class for that. Note that request parameters my look like a numbers to you, but they are in fact strings. That's why converter below accepts String? and returns Status?. That means, it would be convenient for you if your enum accept it as well. Example: NORMAL("0"), ERROR("1").
class ConvStringToStatus : Converter<String?, Status?> {
override fun convert(source: String?) = Status.of(source)
}
To make it work, converter must be registered as below.
#Configuration
class WebConfig : WebMvcConfigurer {
override fun addFormatters(registry: FormatterRegistry) {
registry.addConverter(ConvStringToStatus())
}
}
You just use the name.
ex) status = NORMAL
And does value matter? You can use ordinal
enum class Status{
NORMAL,
ERROR,
}
println(NORMAL.ordinal)
//result:0
This is a joke, but if you need
enum class Status(val value:String){
`0`("NORMAL"),`1`("ERROR")
}
println(data.status.value)
For anyone still looking and finding this. The example code works and does not throw non-null field is null exception if you add jackson-module-kotlin dependency. It should match your com.fasterxml.jackson.core version
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
<version>2.9.8</version>
</dependency>

Customization of Spring ConversionFailedException error HTTP status

I have an enum class:
class enum Type {
LOCAL, REMOTE
}
I have an API that accepts the enum as a GET parameter
#RequestMapping(method = RequestMethod.GET, location="item", params = "type")
public Item[] get(Type type) {
...
When a client calls the API with valid values, like GET /item?type=LOCAL or GET /item?type=REMOTE it works fine. If the client supplies invalid value for type, e.g. GET /item?type=INVALID_TYPE, then Spring generates 500 Internal Server Error. I would like to turn it into 400 Bad Request validation error, potentially adding useful information for the client. I prefer to reuse the built type converter since in works just fine, just want to change a type of error HTTP thrown with minimum changes.
I believe if you add the right exception to #ControllerAdvice, you can customize the response. In this case, I found that MethodArgumentTypeMismatchException was the one in question.
#ExceptionHandler(MethodArgumentTypeMismatchException.class)
public void methodArgumentTypeMismatchException(final HttpServletResponse response) throws IOException {
response.sendError(BAD_REQUEST.value());
}
Why is this happening?
I would consider having a look at the example here about the #ControllerAdvice and/or #ExceptionHandler annotations. The error you're experiencing is occurring because, I believe, Spring tries to construct a Type from the "INVALID_TYPE" string and gets an error when it cannot create a Type from it--because "INVALID_TYPE" is not one of the available values.
What can I do about it?
What you'll want to do is add a string constructor to your enum so it knows, more correctly how to create one of the enum objects, and then check the input to see if its valid. If it is invalid, throw a custom exception. Then, in your #ControllerAdvice, you can customize the HTTP status code of the response.
The exception will then be able to be handled with something like the following:
#ControllerAdvice
class GlobalControllerExceptionHandler {
#ResponseStatus(HttpStatus.BAD_REQUEST) // 409
#ExceptionHandler(MyCustomException.class)
public void handleConflict() {
// handle the exception response, if you need information about the
// request, it should be able to be attached to the custom exception
}
}
The enum would look something like this:
public enum Type{
LOCAL("LOCAL"),
REMOTE("REMOTE");
private String type;
private Type(String type) {
if(type.equals("LOCAL") || type.equals("REMOTE")) {
this.type = type;
} else {
throw new MyCustomException();
}
}
public String getType() {
return url;
}
}

How do you handle deserializing empty string into an Enum?

I am trying to submit a form from Ext JS 4 to a Spring 3 Controller using JSON. I am using Jackson 1.9.8 for the serialization/deserialization using Spring's built-in Jackson JSON support.
I have a status field that is initially null in the Domain object for a new record. When the form is submitted it generates the following json (scaled down to a few fields)
{"id":0,"name":"someName","status":""}
After submitted the following is seen in the server log
"nested exception is org.codehaus.jackson.map.JsonMappingException: Can not construct instance of com.blah.domain.StatusEnum from String value '': value not one of the declared Enum instance names"
So it appears that Jackson is expecting a valid Enum value or no value at all including an empty string. How do I fix this whether it is in Ext JS, Jackson or Spring?
I tried to create my own ObjectMapper such as
public class MyObjectMapper extends Object Mapper {
public MyObjectMapper() {
configure(DeserializationConfig.Feature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
}
}
and send this as a property to MappingJacksonMappingView but this didn't work. I also tried sending it in to MappingJacksonHttpMessageConverter but that didn't work. Side question: Which one should I be sending in my own ObjectMapper?
Suggestions?
The other thing you could do is create a specialized deserializer (extends org.codehaus.jackson.map.JsonDeserializer) for your particular enum, that has default values for things that don't match. What I've done is to create an abstract deserializer for enums that takes the class it deserializes, and it speeds this process along when I run into the issue.
public abstract class EnumDeserializer<T extends Enum<T>> extends JsonDeserializer<T> {
private Class<T> enumClass;
public EnumDeserializer(final Class<T> iEnumClass) {
super();
enumClass = iEnumClass;
}
#Override
public T deserialize(final JsonParser jp,
final DeserializationContext ctxt) throws IOException, JsonProcessingException {
final String value = jp.getText();
for (final T enumValue : enumClass.getEnumConstants()) {
if (enumValue.name().equals(value)) {
return enumValue;
}
}
return null;
}
}
That's the generic class, basically just takes an enum class, iterates over the values of the enum and checks the next token to match any name. If they do it returns it otherwise return null;
Then If you have an enum MyEnum you'd make a subclass of EnumDeserializer like this:
public class MyEnumDeserializer extends EnumDeserializer<MyEnum> {
public MyEnumDeserializer() {
super(MyEnum.class);
}
}
Then wherever you declare MyEnum:
#JsonDeserialize(using = MyEnumDeserializer.class)
public enum MyEnum {
...
}
I'm not familiar with Spring, but just in case, it may be easier to handle that on the client side:
Ext.define('My.form.Field', {
extend: 'Ext.form.field.Text',
getSubmitValue: function() {
var me = this,
value;
value = me.getRawValue();
if ( value === '' ) {
return ...;
}
}
});
You can also disallow submitting empty fields by setting their allowBlank property to false.
Ended up adding defaults in the EXT JS Model so there is always a value. Was hoping that I didn't have to this but it's not that big of a deal.
I have the same issue. I am reading a JSON stream with some empty strings. I am not in control of the JSON stream, because it is from a foreign service. And I am always getting the same error message. I tried this here:
mapper.getDeserializationConfig().with(DeserializationConfig.Feature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
But without any effect. Looks like a Bug.

Resources