How do I send data (JSON or XML) and deserialize it in grails controller?
I tried render params and render request.JSON whitch returned me empty json, and also the command approach
import grails.converters.JSON
class TestController {
def test(RestCommand r){
render r as JSON
}
}
class RestCommand {
String data
static constraints = {
data nullable: false
}
}
witch resulted in
{"data":null,"errors":{"errors":[{"object":"x.x.RestCommand","field":"data","rejected-value":null,"message":"Property [data] of class [class x.x.RestCommand] cannot be null"}]}}
here's my curl request:
curl -X POST --header "Content-Type:application/json" --data '{"id":1}' localhost:8080/myApp/myController/myMethod
P.S. I have viewed similar questions in stackexchange, but as I've discussed above, none of the approaches worked.
Grails provides serialized object for you, your data just has to have the same fields as command.
This works for me:
import grails.converters.JSON
class TestController {
def test(RestCommand r){
if(r.hasErrors()) {
render (
status: 400,
text: r.getErrors() as JSON)
} else {
render "id: "+r.id+" data: "+r.someData
}
}
}
class RestCommand {
Integer id
String someData
static constraints = {
id nullable: false
}
}
Here's the request:
curl -X POST --header "Content-Type:application/json" --data '{"id":1,"someData":"here you go some data"}' localhost:8080/myapp/test/test
results:
id: 1 data: here you go some data
They way you are doing it is working (sending JSON to a Controller and deserializing it). If you send JSON to this action, the command will bind the received data (like it does in your example). Your problem is that the data in your curl test is invalid. Normally, you would check if the Command has errors like :
def test(RestCommand r){
if(r.hasErrors()) {
// handle error
response.status = 400
render "error"
return
}
render "ok"
}
Related
I am trying to process a POST request with body of plain text (utf-8) but it seems that spring does not like the plain text nature of the call. Could it be that it is not supported - or otherwise, am I coding it wrong?
#RestController
#RequestMapping(path = "/abc", method = RequestMethod.POST)
public class NlpController {
#PostMapping(path= "/def", consumes = "text/plain; charset: utf-8", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Object> doSomething(#RequestBody String bodyText)
{
...
return ResponseEntity.ok().body(responseObject);
}
}
Respond is:
Resolved [org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'application/x-www-form-urlencoded' not supported]
I tested with curl command:
curl -s -X POST -H 'Content-Type: text/plain; charset: utf-8' --data-binary #text.txt localhost:8080/abc/def
The text.txt contains plain text (UTF-8 in Hebrew).
I would like to throw some light on the other part of the question of whether spring supports text/plain?
According to spring docs: what is "consumes" or Consumable Media Types in the #RequestMapping annotation
Definition :
Consumable Media Types
"You can narrow the primary mapping by specifying a list of consumable media types. The request will be matched only if the Content-Type request header matches the specified media type. For example:"
#Controller
#RequestMapping(value = "/pets", method = RequestMethod.POST, consumes="application/json")
public void addPet(#RequestBody Pet pet, Model model) {
// implementation omitted
}
Consumable media type expressions can also be negated as in !text/plain to match to all requests other than those with Content-Type of text/plain.
How spring does the media type matching internally?
Spring Request Driven Design is centred around a servlet called the dispatcher Servlet which uses special beans to process requests one of the bean is RequestMappingHandlerMapping which checks for the media type using
getMatchingCondition method of ConsumesRequestCondition class as shown below.
#Override
public ConsumesRequestCondition getMatchingCondition(ServerWebExchange exchange) {
if (CorsUtils.isPreFlightRequest(exchange.getRequest())) {
return PRE_FLIGHT_MATCH;
}
if (isEmpty()) {
return this;
}
Set<ConsumeMediaTypeExpression> result = new LinkedHashSet<>(expressions);
result.removeIf(expression -> !expression.match(exchange));
return (!result.isEmpty() ? new ConsumesRequestCondition(result) : null);
}
the get matching condition class uses static inner class ConsumesMediaType Expression which actually makes the check
#Override
protected boolean matchMediaType(ServerWebExchange exchange) throws UnsupportedMediaTypeStatusException {
try {
MediaType contentType = exchange.getRequest().getHeaders().getContentType();
contentType = (contentType != null ? contentType : MediaType.APPLICATION_OCTET_STREAM);
return getMediaType().includes(contentType);
}
catch (InvalidMediaTypeException ex) {
throw new UnsupportedMediaTypeStatusException("Can't parse Content-Type [" +
exchange.getRequest().getHeaders().getFirst("Content-Type") +
"]: " + ex.getMessage());
}}
This method returns false once the media type does not match and getMatchingCondition returns null which results in handleNoMatch method of RequestMappingInfoHandlerMapping being called and using PartialMatchHelper class we check for the what type of mismatch it has as shown below and spring throws HttpMediaTypeNotSupportedException error once its see consumes mismatch
if (helper.hasConsumesMismatch()) {
Set<MediaType> mediaTypes = helper.getConsumableMediaTypes();
MediaType contentType = null;
if (StringUtils.hasLength(request.getContentType())) {
try {
contentType = MediaType.parseMediaType(request.getContentType());
}
catch (InvalidMediaTypeException ex) {
throw new HttpMediaTypeNotSupportedException(ex.getMessage());
}
}
throw new HttpMediaTypeNotSupportedException(contentType, new ArrayList<>(mediaTypes));
}
Spring supports all media types as per the IANA https://www.iana.org/assignments/media-types/media-types.xhtml the problem lies only with the curl command as quoted by others.
#rumbz
Please refer to the below link it might solve your issue
Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported for #RequestBody MultiValueMap
1 Using annotation
#RequestMapping(value = "/some-path", produces =
org.springframework.http.MediaType.TEXT_PLAIN)
public String plainTextAnnotation() {
return "<response body>";
}
where replace /some-path with whatever you'd like to use.
2 Setting content type in the response entity's HTTP headers:
public String plainTextResponseEntity() {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(org.springframework.http.MediaType.TEXT_PLAIN);
return new ResponseEntity("<response body>", httpHeaders, HttpStatus.OK);
}
Per #m-deinum 's comment: The problem is not in the spring framework - but in the fact that curl adds "application/x-www-form-urlencoded" to the request ...
And just to make this question complete:
curl -s -X POST -H "Content-Type:" -H "Content-Type: text/plain; charset: utf-8" --data-binary #file.txt localhost:8080/abc/def
I have a basic SpringBoot 2.0.6.RELEASE app. Using Spring Initializer, JPA, embedded Tomcat, Thymeleaf template engine, and package as an executable JAR with a restful architecture
#PutMapping(path = "/users/alarms2", consumes = "application/json", produces = "application/json")
public void setAlerts2(#RequestHeader(value = "Authorization") String authHeader,
#RequestBody AlertConfiguration alertConfiguration)
throws DataAccessException, ClientProtocolException, SQLException, IOException {
..
}
but when I call this method from curl:
curl -X PUT -H "Content-Type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJyaWNhcmQub2SAsZUBnbWFpbC5jb20iLCJleHAiOjE2MDAxODMzNDAsImlhdCI6MTUzOTcwMzM0MH0.2gbyyGnkcoHjOw7HbUBBQgb59Bw8iAyFbqTe2DPUlOA-V5UwrW3KXWHZlXssZni8oRJ_o1QRzAFtAWMfz7f0Yw" -d ‘{“symbol": “MENU”, "alarmKey”:”VEGAN” , "enabled": "true"}' "http://95.90.225.68:1133/restos/api/v1/users/alarms2"
I got this error in the server:
2018-10-17 17:16 [http-nio-1133-exec-9] WARN o.s.w.s.m.m.a.ExceptionHandlerExceptionResolver.resolveException(140) - Resolved [org.springframework.web.method.annotation.MethodArgumentTypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'long'; nested exception is java.lang.NumberFormatException: For input string: "alarms2"]
You probably have another method with a more general signature that overlaps given one. Something like:
#PutMapping(path = "/users/{userId}", ...)
public void someMethod(#PathVariable(value = "userId") Long userId) {
...
}
So when you consume /api/v1/users/alarms2 Spring first tries to convert "alarms2" (which is obviously not a valid Long) to userId (which is Long)
I work on small test project to check how Spring Reactive Web Applications actually works with MongoDB.
I follow the manual from https://docs.spring.io/spring/docs/5.0.0.M4/spring-framework-reference/html/web-reactive.html
and it states that I can process POST request in controller like:
#PostMapping("/person")
Mono<Void> create(#RequestBody Publisher<Person> personStream) {
return this.repository.save(personStream).then();
}
Though this seems not works. Here the controller I implemented:
https://github.com/pavelmorozov/reactor-poc/blob/master/src/main/java/com/springreactive/poc/controller/BanquetHallController.java
it have just one POST mapping and it is very simple:
#PostMapping("/BanquetHall")
Mono<Void> create(#RequestBody Publisher<BanquetHall> banquetHallStream) {
return banquetHallRepository.insert(banquetHallStream).then();
}
It is called each time I issue a POST with curl:
curl -v -XPOST -H "Content-type: application/json" -d '{"name":"BH22"}' 'http://localhost:8080/BanquetHall'
Note: Unnecessary use of -X or --request, POST is already inferred.
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> POST /BanquetHall HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.47.0
> Accept: */*
> Content-type: application/json
> Content-Length: 15
>
* upload completely sent off: 15 out of 15 bytes
< HTTP/1.1 200 OK
< content-length: 0
<
* Connection #0 to host localhost left intact
And I see new objects stored in mongodb, but they not contain data. To debug I build simple subscriber, to see the data actually passed as request body to controller:
Subscriber s = new Subscriber() {
#Override
public void onSubscribe(Subscription s) {
logger.info("Argument: "+s.toString());
}
#Override
public void onNext(Object t) {
logger.info("Argument: "+t.toString());
}
#Override
public void onError(Throwable t) {
logger.info("Argument: "+t.toString());
}
#Override
public void onComplete() {
logger.info("Complete! ");
}
};
banquetHallStream.subscribe(s);
and now I see after subscription onError method called. The Throwable states body missing:
Here error string:
Request body is missing: reactor.core.publisher.Mono<java.lang.Void> com.springreactive.poc.controller.BanquetHallController.create(org.reactivestreams.Publisher<com.springreactive.poc.domain.BanquetHall>)
Why request body is empty?
Also good to know: As I new with all this reactive stuff, could it be some better approach to debug Publisher/Subscriber without manual implementing Subscriber?
Update I updated POST handler method description and it passes request body as String object:
Mono<Void> create(#RequestBody String banquetHallStream)
Then this is not a "Reactive", right? String is not reactive, as Publisher should be...
I had exact the same issue and was able to solve it by putting #ResponseStatus on method. Below is how method controller looks like:
#ResponseStatus(HttpStatus.CREATED)
#PostMapping(value = "/bulk", consumes = APPLICATION_STREAM_JSON_VALUE)
public Mono<Void> bulkInsert(#RequestBody Flux<Quote> quotes) {
return quoteReactiveRepository.insert(quotes).then();
}
I'm doing the request to that endpoint using WebClient:
webClient.post()
.uri("/quotes/bulk")
.contentType(MediaType.APPLICATION_STREAM_JSON)
.body(flux(), Quote.class)
.retrieve()
.bodyToMono(Void.class).block();
tested with: org.springframework.boot:spring-boot-starter-webflux:2.1.0.RELEASE
I am trying to understand what is the reason of existence #JsonInclude to use it in my DTOs. Let's look at this simple example:
A Code:
class DemoApplication {
static void main(String[] args) {
SpringApplication.run DemoApplication, args
}
#PostMapping("/")
String greet(#RequestBody Greeting greeting) {
return "Hello ${greeting.name}, with email ${greeting.email}"
}
}
#JsonInclude(JsonInclude.Include.NON_NULL)
class Greeting {
String name
String email
}
B Code:
class DemoApplication {
static void main(String[] args) {
SpringApplication.run DemoApplication, args
}
#PostMapping("/")
String greet(#RequestBody Greeting greeting) {
return "Hello ${greeting.name}, with email ${greeting.email}"
}
}
class Greeting {
String name
String email
}
The only difference between the A code and the B code is that in the B code the greeting class does not use the #JsonInclude annotation.
But if I do some simple CURL requests to that endpoint (A code and B code):
~ curl -H "Content-Type: application/json" -X POST localhost:8080
{"timestamp":"2018-04-22T21:18:39.849+0000","status":400,"error":"Bad Request","message":"Required request body is missing: public java.lang.String com.example.demo.DemoApplication.greet(com.example.demo.Greeting)","path":"/"}
~ curl -H "Content-Type: application/json" -X POST localhost:8080 -d '{}'
Hello null, with email null
~ curl -H "Content-Type: application/json" -X POST localhost:8080 -d '{"name": "AlejoDev"}'
Hello AlejoDev, with email null
~ curl -H "Content-Type: application/json" -X POST localhost:8080 -d '{"name": "AlejoDev", "email":"info#alejodev.com"}'
Hello AlejoDev, with email info#alejodev.com
Get the same behavior, then what is the utility of using #JsonInclude annotation?
For example, I expected that in A code when I sent a request with only one field, like this:
~ curl -H "Content-Type: application/json" -X POST localhost:8080 -d '{"name": "AlejoDev"}'
the Greeting object became an object with only one field, and not an object with two fields. one full and one empty.
What that annotation means is that when an object is deserialized whether to include only non-null fields on not.
When you are using it for an object that you are getting as an input, even if the input fields are null, because the variables name and email are class level fields, they will be inited to their default value which is null.
So when you create your return statement you are always going to get null for those.
Try this annotation with a JSON object as a response and then try populating some fields and keeping some fields null and then see the response to various calls. This will help you understand the effects of this annotation.
#JsonInclude(Include.NON_NULL) or #JsonInclude(JsonInclude.Include.NON_NULL)
is used to ignore null fields in an object.
In your particular example you have returned a String value that is why it is printing null.
If you try to return the complete object, then you will find that the null fields are not included in the response body
I have this Jersey POST resource :
#POST
#Consumes(MediaType.APPLICATION_XML)
#Produces(MediaType.APPLICATION_XML)
public Response blablabla(InputStream inputStream,
#BeanParam ImportParams importParams) throws IOException, JAXBException, SAXException {
Here is the ImportParams class :
public class ImportParams {
#QueryParam(value = "importType")
public ImportType importType = ImportType.MERGE;
#ApiParam("Force stop point type for all stop points in file. Useful if no modality defined in the netex file.")
#QueryParam(value = "forceStopType")
public StopTypeEnumeration forceStopType;
}
When I use curl to post to the the resource, only the first query parameter I specify after the question mark in the URL is read by jersey :
curl -XPOST -H"Content-Type: application/xml" -H"authorization: bearer $TOKEN" -d#$3 http://localhost:8585/services/stop_places/netex?forceStopType=TRAM_STATION&importType=MERGE
==> forceStopType has the right value, and importType is null
curl -XPOST -H"Content-Type: application/xml" -H"authorization: bearer $TOKEN" -d#$3 http://localhost:8585/services/stop_places/netex?importType=MERGE&forceStopType=TRAM_STATION
==> importType has the right value and forceStopType is null
I've used #BeanParam many times before and it used to work, so I must be missing something obvious.... Thanks for your help
Haha found out - stupid me. Had to put double quotes around the URL when cURLing :
curl -XPOST -H"Content-Type: application/xml" -H"authorization: bearer $TOKEN" -d#$3 "http://localhost:8585/services/stop_places/netex?forceStopType=TRAM_STATION&importType=MERGE"