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
Related
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");
}
getUserDetails Method returns Mono of Type JsonNode. But I Actually want to return a Mono<User.java> or Flux<User.java>. please help modifying getBulkUserInfo or getUserDetails to get the Mono<User.java> or Flux<User.java>
public Mono<JsonNode> getUser(BigInteger Id){
return this.client.get()
.uri("/URL/{Id}",Id)
.retrieve()
.bodyToMono(JsonNode.class);
}
public Flux getBulkUsers(List<BigInteger> Ids){
return Flux.fromIterable(Ids).flatMap(this::getUser);
}
But The json response from the Url is something like
{
"resultholder": {
"totalResults": "1",
"profiles": {
"profileholder": {
"user": {
"country": "IND",
"zipCode": "560048",
"name":"Test"
}
}
}
}
}
I tried different ways but nothing worked subscribe() and
.doOnNext(resp -> resp.get("resultholder").get("profiles").get("profileholder").get("user"))
.bodyToMono(JsonNode.class)
.doOnNext(resp ->{return
JSONUtils.deserialize(resp.get("resultholder").get("profiles").get("profileholder").get("user"), User.class)})
This is pretty straightforward and there is no need to block. Its just applying further mappings on the response. You can use the following code for your problem
return webClient
.get()
.uri("profilesEndPoint/" + id)
.retrieve()
.bodyToMono(JsonNode.class)
.map(jsonNode ->
jsonNode.path("resultholder").path("profiles").path("profileholder").path("user")
).map(
userjsonNode -> mapper.convertValue(userjsonNode, User.class)
);
Where mapper is jackson ObjectMapper
private final ObjectMapper mapper = new ObjectMapper();
If you have any issues please refer to this code here :
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"));
}
})
)
Code sample:-
public List<UserDto> getUserCandidates(String taskId) {
List<UserCandidates> listResponse;
ResponseEntity<String> response=restTemplate.getForEntity(configProperties.getUrl()+"/task/"+taskId+"/identity-links",
String.class);
listResponse =new Gson().fromJson(response.getBody(), new TypeToken<ArrayList<UserCandidates>>(){}.getType());
listResponse.forEach(result->{
if(!StringUtils.isEmpty(result.getUserId())){
ResponseEntity<UserRefer> userResponse=restTemplate.getForEntity(configProperties.getUrl()+"/user/"+result.getUserId()+"/profile", UserRefer.class);
userDtoList.add(new UserDto(result.getUserId(), Arrays.asList(result.getGroupId()), Arrays.asList(result.getType()), userResponse.getBody().getFirstName(),
userResponse.getBody().getLastName(), userResponse.getBody().getEmail()));
}
else if(!StringUtils.isEmpty(result.getGroupId())) {
ResponseEntity<String> responseGroup=restTemplate.getForEntity(configProperties.getUrl()+"/user"+"?memberOfGroup="+result.getGroupId(), String.class);
List<UserResponse> listGroup=new Gson().fromJson(responseGroup.getBody(), new TypeToken<ArrayList<UserResponse>>(){}.getType());
listGroup.forEach(resultGroup->{
userDtoList.add(new UserDto(resultGroup.getId(),Arrays.asList(result.getGroupId()),
Arrays.asList(result.getType()),resultGroup.getFirstName(),resultGroup.getLastName(),resultGroup.getEmail()));
});
}
});
return userDtoList;
}
So in if condition the response from API I'm getting is
UserRefer(id=demo, firstName=Demo, lastName=Demo, email=demo#camunda.org) - userResponse object
And from listResponse object data is [UserCandidates(userId=null, groupId=accounting, type=candidate), UserCandidates(userId=null, groupId=sales, type=candidate), UserCandidates(userId=demo, groupId=null, type=assignee)]
next in else if condition the response for listGroup is [UserResponse(status=null, id=demo, firstName=Demo, lastName=Demo, email=demo#camunda.org), UserResponse(status=null, id=mary, firstName=Mary, lastName=Anne, email=mary#camunda.org)]
So now you can see the data is duplicate. The output i want is for when userId is not empty from the data it should take type and merge the array
else if grouped not empty the data it should take for groupType and merge in the array removing duplicte and merging in same object
Output :-
[
{
"userId": "demo",
"name": "Demo Demo",
"type": [
"candidate",
"assignee"
],
"email": "demo#camunda.org",
"groupId": [
"accounting",
"sales"
]
},
{
"userId": "mary",
"name": "Mary Anne",
"type": [
"candidate"
],
"email": "mary#camunda.org",
"groupId": [
"accounting",
"sales"
]
}
]
You need some fundamental changes in your code.
1- instead of using ResponseEntity<String> use ResponseEntity<UserCandidates[]> response by this changing you don't need use Gson() dependency.
2- You don't need to use StringUtils to check to be empty. there is same method for both string and list objects.
3- For the duplicate date I define a Map<String,UserDto> with id as key and userDto object as a value. and where the userDto data is created I store it in the map with the id. as you see for storing userDto object in the map I used merge method that for the duplicate key(id) it has a merge function.
Tip: for readability would be nice to separate the restTemplate call in other class may you reuse it too.
mergeFunction is somthing like this:
private UserDto mergeFunction(UserDto u1,UserDto u2){
u1.getType().addAll(u2.getType());
u1.getGroupId().addAll(u2.getGroupId());
return u1;
}
and complete code is:
public List<UserDto> getUserCandidates(String taskId) {
Map<String, UserDto> userDtoMap = new HashMap<>();
Map<String, String> params = new HashMap<>();
ResponseEntity<UserCandidates[]> response = restTemplate
.getForEntity(configProperties.getUrl() + "/task/" + taskId + "/identity-links",
UserCandidates[].class, params);
Arrays.asList(response.getBody()).forEach(result -> {
if (!result.getUserId().isEmpty()) {
ResponseEntity<UserRefer> userResponse = restTemplate
.getForEntity(configProperties.getUrl() + "/**", UserRefer.class);
userDtoMap.merge(result.getUserId(), new UserDto(result.getUserId(),
new ArrayList<>(Arrays.asList(result.getGroupId())), Arrays.asList(result.getType()),
userResponse.getBody().getFirstName(),
userResponse.getBody().getLastName(),
userResponse.getBody().getEmail()), (u1, u2) -> mergeFunction(u1,u2));
} else if (!result.getGroupId().isEmpty()) {
String requestUri = configProperties.getUrl() + "/user" +
"?memberOfGroup={memberOfGroup}";
Map<String, String> userResParam = new HashMap<>();
userResParam.put("memberOfGroup", result.getGroupId());
ResponseEntity<UserResponse[]> responseGroup = restTemplate
.getForEntity(requestUri, UserResponse[].class, userResParam);
Arrays.asList(responseGroup.getBody()).forEach(resultGroup -> {
userDtoMap.merge(resultGroup.getId(), new UserDto(resultGroup.getId(),
Arrays.asList(result.getGroupId()),
Arrays.asList(result.getType()), resultGroup.getFirstName(),
resultGroup.getLastName(),
resultGroup.getEmail()), (u1, u2) -> mergeFunction(u1,u2));
});
}
});
return new ArrayList<>(userDtoMap.values());
}
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)