I have the following JSON:
{"certificates":[
{
"fileType": "pdf",
"binaryFile": "dasdasdasdas",
"owner": {
"namePerson": "Diego Pérez",
"documentType": "TI",
"documentNumber": "1234556650"
}
},
{
"fileType": "pdf",
"binaryFile": "dasdasdasdas",
"owner": {
"namePerson": "Juan Pérez",
"documentType": "PS",
"documentNumber": "1024556650"
}
}
]}
It is a JsonArray which contains CertificateObject but I can not directly read it as it, I had some errors so I had to do the following (receive it as an String and do conversion):
#PostMapping("/certificates")
public ResponseEntity<String> postCertificates(#RequestBody String certificates)
throws JsonMappingException, JsonProcessingException {
JsonObject convertedObject = new Gson().fromJson(certificates, JsonObject.class);
log.info(convertedObject.get("certificates"));
List<CertificateObject> defunctionCertificates = new ObjectMapper().readValue(
convertedObject.get("certificates").toString(), new TypeReference<List<CertificateObject>>() {
});
return ResponseEntity.ok("ok");
}
The problem is that I would like to be able to read it directly as an array (java List) like so:
#PostMapping("/certificates")
public ResponseEntity<String> postCertificates(#RequestBody List<CertificateObject> certificates)
throws JsonMappingException, JsonProcessingException {
// no need to do any conversion to the certificates
return ResponseEntity.ok("ok");
}
Please let me know if you need more details (the CertificateObject class or something else) to help me with this, thank you!
I think you can have a wrapper class which could get you list of certificates.
#Getter //Lombok annotation
#Setter
#Builder
public class CertificatesWrapper{
private List<Certificate> certificates;
}
Add this to your endpoint request body.
#PostMapping("/certificates")
public ResponseEntity<String> postCertificates(#RequestBody CertificateWrapper certificateWrapper)
throws JsonMappingException, JsonProcessingException {
// no need to do any conversion to the certificates
List<Certificate> certs = certificateWrapper.getCertificates(); // gives you certs
return ResponseEntity.ok("ok");
}
I think the better way would be to send the request as a list of jsob objects rather than json obj containing a list of json objects. If so you wouldn't require this wrapper.
[
{
"fileType": "pdf",
"binaryFile": "dasdasdasdas",
"owner": {
"namePerson": "Diego Pérez",
"documentType": "TI",
"documentNumber": "1234556650"
}
},
{
"fileType": "pdf",
"binaryFile": "dasdasdasdas",
"owner": {
"namePerson": "Juan Pérez",
"documentType": "PS",
"documentNumber": "1024556650"
}
}
]
I think this helps !!!.
You can convert your body request to a JsonNode object and then read the selected JsonNode certificates property into a List<CertificateObject> list calling the ObjectMapper#readerFor method:
#PostMapping("/certificates")
public ResponseEntity<String> postCertificates(#RequestBody JsonNode root) throws IOException {
ObjectReader reader = new ObjectMapper().readerFor(new TypeReference<List<CertificateObject>>() {
});
//reading the certificates property into a list
List<CertificateObject> list = reader.readValue(root.at("/certificates"));
return ResponseEntity.ok("ok");
}
Related
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 trying to parse json body request coming in for a post request using Spring Boot. I would like to map the body to fields on vehicle class and also to store plain json body to some variable as well for future use. But i am always getting stream closed exception when trying to access plain json body. Can someone help me out on this. Thanks In Advance
Code
#RequestMapping(value = "/GetDriverDetails", method = RequestMethod.POST)
public ResponseEntity<Vehicle> GetVehicleDetails(#RequestBody Vehicle vehicle, HttpServletRequest request) {
System.out.println(vehicle);
String json;
if ("POST".equalsIgnoreCase(request.getMethod()))
{
try {
ContentCachingRequestWrapper request1 = new ContentCachingRequestWrapper(request);
String collect = request1.getReader().lines().collect(Collectors.joining(System.lineSeparator()));
System.out.println(collect);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return new ResponseEntity<Vehicle>(HttpStatus.OK);
}
Json request Body
{
"vehicleName": "Brio",
"vehicleModel": "fisrtClass",
"drivers": [
{
"name": "rej",
"licenseNumber": "KLLicense1"
},
{
"name": "Dan",
"licenseNumber": "KLLicense2"
},
{
"name": "bala",
"licenseNumber": "KLLicense3"
},
{
"name": "vijay",
"licenseNumber": "KLLicense4"
},
{
"name": "aravind",
"licenseNumber": "KLLicense5"
},
{
"name": "sathya",
"licenseNumber": "KLLicense6"
}
]
}
Exception
java.io.IOException: Stream closed
at org.apache.catalina.connector.InputBuffer.read(InputBuffer.java:359) ~[tomcat-embed-core-9.0.38.jar:9.0.38]
at org.apache.catalina.connector.CoyoteInputStream.read(CoyoteInputStream.java:132) ~[tomcat-embed-core-9.0.38.jar:9.0.38]
at org.springframework.web.util.ContentCachingRequestWrapper$ContentCachingInputStream.read(ContentCachingRequestWrapper.java:254) ~[spring-web-5.2.9.RELEASE.jar:5.2.9.RELEASE]
at java.base/sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:297) ~[na:na]
at java.base/sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:339) ~[na:na]
at java.base/sun.nio.cs.StreamDecoder.read(StreamDecoder.java:188) ~[na:na]
at java.base/java.io.InputStreamReader.read(InputStreamReader.java:181) ~[na:na]
Can you try following code:
The solution to your main problem, since you are using #RequestBody, contents are already read and mapped to pojo class hence stream is utlized and closed in this case you do not want to use #RequestBody at all. Please find my implementation below:
#PostMapping(path = "update-vehicle-details", consumes = MediaType.ALL_VALUE)
public VehicleDriver updateVehicleDetails(HttpServletRequest request) throws IOException {
ContentCachingRequestWrapper request1 = new ContentCachingRequestWrapper(request);
String collect = request1.getReader().lines().collect(Collectors.joining(System.lineSeparator()));
System.out.println(collect);
final VehicleDriver vehicleDriver = new ObjectMapper().readValue(collect, VehicleDriver.class);
return vehicleDriver;
}
Otherwise, use a simple approach, read the value from application json content type parses in requestbody and converts that body to string and return the same result
#RestController
public static class TestController {
#PostMapping(path = "update-vehicle-details", consumes = MediaType.APPLICATION_JSON_VALUE)
public String updateVehicleDetails(#RequestBody VehicleDriver vehicleDriver) throws JsonProcessingException {
final StringBuilder stringBuilder = new StringBuilder(vehicleDriver.vehicleName);
List<String> driverDetails = Optional.ofNullable(
vehicleDriver.drivers)
.map(Collection::stream)
.orElse(Stream.empty())
.map(d -> "name=: " + d.name + ", license number:" + d.licenseNumber)
.collect(Collectors.toList());
stringBuilder.append("\n");
stringBuilder.append(driverDetails);
String stringRepresentationOfBody = new ObjectMapper().writeValueAsString(vehicleDriver);
// return stringBuilder.toString();
return stringRepresentationOfBody;
}
}
public static class VehicleDriver {
public String vehicleName;
public String vehicleModel;
public List<Driver> drivers;
}
public static class Driver {
public String name;
public String licenseNumber;
}
Try using Object Mapper for converting your vehicle object to json string and in that case you would not be needing request in method argument.
And you are using post request method then if condition is not needed.
I have a simple requirement where, if application encounters an exception, my JAX-RS Rest endpoint should return a custom JSON response with 500 HTTP header status.
Data needed to construct the response comes from an object with several properties (see below). The problem is, I am only interested in one or two values from each property (out of several dozens). And I cannot modify any of these models/classes (some have a Jackson annotation for JSON processing, e.g. null properties should be discarded during serialization).
public class MainObject {
private FirstProperty firstProperty;
private SecondProperty secondProperty;
private ThirdProperty thirdProperty;
// other codes
public String toString() {
ObjectMapper mapper = new ObjectMapper();
try { return mapper.writeValueAsString(this); }
catch (Exception e) { return null; }
}
}
public class FirstProperty {
private boolean bol = true;
private double dob = 5.0;
private List<String> subProperty;
// other properties
public String toString() {
ObjectMapper mapper = new ObjectMapper();
try { return mapper.writeValueAsString(this); }
catch (Exception e) { return null; }
}
}
#JsonSerialize(include=JsonSerialize.Inclusion.NON_NULL)
public class SecondProperty {
private String str;
private List<String> subProperty;
// other properties
public String toString() {
ObjectMapper mapper = new ObjectMapper();
try { return mapper.writeValueAsString(this); }
catch (Exception e) { return null; }
}
}
#JsonSerialize(include=JsonSerialize.Inclusion.NON_NULL)
public class ThirdProperty {
private int intProp = true;
private List<String> subProperty;
// other properties
public String toString() {
ObjectMapper mapper = new ObjectMapper();
try { return mapper.writeValueAsString(this); }
catch (Exception e) { return null; }
}
}
The expected JSON that I should be seeing coming back is on the client side (say, a browser -- testing in Edge):
{
"firstProperty" : { "subProperty" : [ "val1" ] },
"secondProperty" : { "str" : "val2", "subproperty" : [ "val3", "val6" ] },
"thirdProperty" : { "subProperty" : [ "val4" ] }
}
Instead, all my field names and their values have their quotations escaped, and extra double quotes around the entire value, e.g.:
{
"firstProperty" : "{ \"subProperty\" : [ \"val1\" ] }",
"secondProperty" : "{ \"str\" : \"val2\", \"subproperty\" : [ \"val3\", \"val6\" ] }",
"thirdProperty" : "{ \"subProperty\" : [ \"val4\" ] }"
}
Please note the extra " before and after the curly brackets. My environment is:
Java 1.8.45
FasterXML Jackson 2.9.8
Spring Boot 2.0.1
RestEasy (JBoss) JAX-RS
JBoss 6.4
I eliminated the majority of "noise" in the code to see at what point this happens. This is the controller:
#Path("/")
public class MainController {
#GET
#Produces(MediaType.APPLICATION_JSON)
#Path("/rest/path")
public MainObject getMainObject throws MyCustomException {
// A service call that throws MyCustomException
}
}
And JAX-RS ExceptionMapper where I send the response back:
#Provider
public class MyCustomExceptionMapper extends ExceptionMapper<MyCustomException> {
#Override
public Response toResponse(MyCustomException ex) {
HashMap<String, Object> responseBody = new HashMap<>();
String strEx = ex.getStrEx(); // Comes from SecondProperty.str stored in MyCustomException, not that it matters
// Instantiate an empty object that contains
MainObject obj = new MainObject();
obj.getFirstProperty().setSubProperty(ex.getStrs());
obj.getSecondProperty().setStr(strEx);
obj.getSecondProperty().setSubProperty(ex.getStrs());
obj.getThirdProperty().setSubProperty(ex.getStrs());
responseBody.put("firstProperty", serializeFirstProperty(obj.getFirstProperty()));
responseBody.put("secondProperty", serializeSecondProperty(obj.getSecondProperty()));
responseBody.put("thirdProperty", serializeThirdProperty(obj.getThirdProperty()));
Response response = Response.status(/* 500 status */).entity(responseBody).build();
return response;
}
}
Since I only need to send back a very small subset of overall properties from each of my types, I created a custom StdSerializer that would only populate a needed property. For brevity, I only do serializeFirstProperty() but they are all more or less identical:
private StdSerializer<FirstProperty> getFPSerializer(FirstProperty firstProperty) {
return new StdSerializer<FirstProperty>(FirstProperty.class) {
#Override
public void serialize(FirstProperty value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
if (/* there are items in FirstProperty.subProperty */) {
gen.writeArrayFieldStart("subProperty");
for (String str : value.getSubProperty()) {
gen.writeString(str);
}
gen.writeEndArray();
}
gen.writeEndObject();
}
}
private <T> ObjectMapper getCustomOM(StdSerializer<?> serializer) {
ObjectMapper mapper = new ObjectMapper();
SimpleModule sm = new SimpleModule();
sm.addSerializer(serializer);
mapper.registerModule(module);
return mapper;
}
Then use these helper methods like:
private String serializeFirstProperty(FirstProperty firstProperty) {
ObjectMapper mapper = getCustomOM(getFPSerializer(firstProperty));
String ser = null;
try { ser = mapper.writeValueAsString(firstProperty); }
catch (JsonProcessingException e) { return null; }
return ser;
}
I have tried countless of configurations with ObjectMapper, e.g. disable(JsonParser.Feature.ALLOW_BACKLASH_ESCAPING_ANY_CHARACTER) (couldn't find any relevant flag for JsonGenerator which I really want to disable in a similar fashion).
Or explicitly returning Object from serializeFirstProperty(), or replacing all the \" with " in serializeFirstProperty() when ser is returned.
Or set custom StdSerializer's JsonGenerator.setCharacterEscapes(new CharacterEscapes() { //... } or play around with JAX-RS Response at no avail. I always seem to get a "string" value with quotations, e.g.:
"firstProperty" : "{ \"subProperty\" : [ \"val1\" ] }"
If I simply just do
responseBody.put("firstProperty", mapper.writeValueAsString(obj.getFirstProperty()));
somehow this produces the right JSON output, however, it includes a lot of unnecessary properties which I don't want in this exception handling case.
Funny thing is, when I peer into response (or responseBody map), everything looks right (I don't see values having double quotations).
Please also note that not only I can't modify the models, but some of their properties are instantiated during creation with default values, so not-null inclusion doesn't work, and they will appear in the final JSON if I don't use a custom serialization.
Does anyone know what's causing this escaped and extra quotations?
I think I misunderstood the question in the first attempt of answering it.
The problem is that you serialize a property as string (using mapper.writeValueAsString(this) and then add it to the responseBody which you think of being string to json object map but it is a string to Java object map. In your case at runtime it is a string mapping to another string (the serialized json object is represented as Java string) and a Java string is a Java object as well.
What you want to do instead is to construct a Java object responseBody instead of a map. It should act as a DTO having all the specific properties etc and then serializing it in in one action using the mapper. Because if you first serialize a property to a json string than it it is just a string from the Java point of view and the mapper has no chance to interpret it as a json object.
Details ---
One of my POJO SomeResponseObject for an api response has attribute
#JsonProperty("s_summary")
private Map<String, SummaryObject> summary
which further has few more attributes. These are summed in json as follows :
{
"s_summary": {
"rewardSubscription": {
"accountId": "XYZ",
"startDate": "2015-12-29T19:00:00+05:30",
"endDate": "2017-06-21T00:00:00+05:30",
"isActive": true,
"entityId": "ID123",
"status": "ACTIVE"
}
}
}
This POJO(json) is further modified by our service to return a RESPONSE as :
{
"rewardSubscription": {
"accountId": "XYZ",
"startDate": "2015-12-29T19:00:00+05:30",
"endDate": "2017-06-21T00:00:00+05:30",
"isActive": true,
"entityId": "ID123",
"status": "ACTIVE"
}
}
Narrowing Down ---
Now when we are writing tests against this API call. We end up being unable to map the response to any specific POJOs(java response class). Test code -
JSONObject responseObject = new JSONObject(responseFromService.getResponseBody())
.getJSONObject("RESPONSE");
ObjectMapper objectMapper = new ObjectMapper();
SomeResponseObject summaryResponse = objectMapper.getObjectMapper()
.readValue(responseObject.toString(), SomeResponseObject.class); // And this wouldn't work.
Question --
Is there any way we can cast the current API response or wrap it somehow to be mapped to the actual POJO(SomeResponseObject.class)?
Thanks in advance.
Problem
You receive an object with a rewardSubscription field, or, in your case, a map, with a rewardSubscription key. You can't convert a map to an object of SomeResponseObject type directly.
Solution
Option 1
Convert json to a map manually and set it to the SomeResponseObject instance:
JSONObject responseObject = new JSONObject(responseFromService.getResponseBody())
.getJSONObject("RESPONSE");
ObjectMapper objectMapper = new ObjectMapper();
Map<String, SummaryObject> summaryMap = objectMapper.readValue(responseObject.toString(), new TypeReference<Map<String, SummaryObject>>() {});
SomeResponseObject response = new SomeResponseObject();
response.setSummaryMap(summaryMap);
Option 2
So as not to manually convert map each time, write a custom deserializer that will handle both cases. The deserialize method should be similar to this:
#Override
public SomeResponseObject deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
JsonNode rootNode = jp.readValueAsTree();
JsonNode sSummaryNode = rootNode.get("s_summary");
if (sSummaryNode != null) {
// Usual case.
return objectMapper.treeToValue(sSummaryNode, SomeResponseObject.class);
} else {
// Special case - when received a map.
Map<String, SummaryObject> summaryMap = objectMapper.readValue(rootNode.toString(), new TypeReference<Map<String, SummaryObject>>() {});
SomeResponseObject response = new SomeResponseObject();
response.setSummaryMap(summaryMap);
return response;
}
}
And then in the code you don't care:
ObjectMapper objectMapper = new ObjectMapper();
SomeResponseObject response = objectMapper.readValue(json, SomeResponseObject.class);
I am trying to receive a webhook via a post request from Stripe Payments. The java method to process it looks like this:
#ResponseBody
#RequestMapping( consumes="application/json",
produces="application/json",
method=RequestMethod.POST,
value="stripeWebhookEndpoint")
public String stripeWebhookEndpoint(Event event){
logger.info("\n\n" + event.toString());
logger.info("\n\n" + event.getId());
return null;
}
But the Stripe Event always comes back with all null values:
<com.stripe.model.Event#315899720 id=null> JSON: {
"id": null,
"type": null,
"user_id": null,
"livemode": null,
"created": null,
"data": null,
"pending_webhooks": null
}
If the method receives a String instead,and using #RequestBody:
#ResponseBody
#RequestMapping( consumes="application/json",
produces="application/json",
method=RequestMethod.POST,
value="stripeWebhookEndpoint")
public String stripeWebhookEndpoint(#RequestBody String json){
logger.info(json);
return null;
}
Here, it prints the json without null values. Here's part of the request being printed:
{
"created": 1326853478,
"livemode": false,
"id": "evt_00000000000000",
"type": "charge.succeeded",
"object": "event",
"request": null,
"data": {
"object": {
"id": "ch_00000000000000",
"object": "charge",
"created": 1389985862,
"livemode": false,
"paid": true,
"amount": 2995,
"currency": "usd",
...
}
But using #RequestBody with a Stripe Event parameter gives a 400: bad syntax.
So why can't I take in the correct type, a Stripe Event, as the parameter?
Here's what I did:
The Java method still takes in the Event as a json String. Then I used Stripe's custom gson adapter and got the Event with:
Event event = Event.gson.fromJson(stripeJsonEvent, Event.class);
Where stripeJsonEvent is the string of json taken in by the webhook endpoint.
public String stripeWebhookEndpoint(#RequestBody String json, HttpServletRequest request) {
String header = request.getHeader("Stripe-Signature");
String endpointSecret = "your stripe webhook secret";
try {
event = Webhook.constructEvent(json, header, endpointSecret);
System.err.println(event);
} catch (SignatureVerificationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//
enter code here
return "";
}
I have been looking for the same answer, so after looking at their own code, here is how they actually do it:
String rawJson = IOUtils.toString(request.getInputStream());
Event event = APIResource.GSON.fromJson(rawJson, Event.class);
APIResource comes from their library (I am using 1.6.5)
In order to abstract all of the deserialization logic out of the controller I did the following:
Created a custom deserializer
public class StripeEventDeserializer extends JsonDeserializer<Event> {
private ObjectMapper mapper;
public StripeEventDeserializer(ObjectMapper mapper) {
this.mapper = mapper;
}
#Override
public Event deserialize(JsonParser jp, DeserializationContext context) throws IOException {
ObjectNode root = mapper.readTree(jp);
Event event = ApiResource.GSON.fromJson(root.toString(), Event.class);
return event;
}
}
I then needed to add that deserializer to my ObjectMapper config:
SimpleModule simpleModule = new SimpleModule();
simpleModule.addDeserializer(Event.class, new StripeEventDeserializer(mapper));
mapper.registerModule(simpleModule);
I could then use #RequestBody correctly on the Spring rest controller:
#PostMapping("/webhook")
public void webhook(#RequestBody Event stripeEvent)