Mapping GraphQL response to class in Java/Spring - java

I'm having some trouble mapping a GraphQL response to a class and I'm probably missing something obvious so I need a second pair of eyes on my code I think.
I have a response that looks like this:
{
"data": {
"Area": [
{
"id": "1"
},
{
"id": "2"
}
]
}
}
My consumer and client looks like this:
public class Consumer {
private final WebClient webClient;
private static final String QUERY = "query { Area { id }}";
public Consumer(WebClient webClient) {
this.webClient = webClient;
}
public AreaResponse.AreaData getAreas() {
var request = new GraphQLRequest(QUERY, null);
var res = webClient.post()
.bodyValue(request)
.retrieve()
.bodyToMono(AreaResponse.class)
.block();
return res.getData();
}
}
And finally my response dto:
#Data
public class AreaResponse {
private AreaData data;
#Data
public class AreaData{
private Collection<Area> Area;
#Data
public class Area{
private String id;
}
}
}
If I map the response to String.class I get the same data as in Postman so there's nothing wrong with the query or the endpoint. But when I try to map to AreaResponse the Area object is null. I am not sure if the hierarchy in my response class is correct or if I should move the Collection a step down?

I think the problem comes from the use of upper-case Area in the response. By default, Jackson will try to map all of the stuff to a lower-case version of the class name (or lower-camelcase).
One way to fix it:
#JsonProperty("Area")
private Collection<Area> Area;
P.S. nested/ inner classes can be static, so public static class AreaData etc.

Related

It is illegal to call this method if the current request is not in asynchronous mode

Errors Occurred:
{
"status" : 1,
"code" : 0,
"message" : "It is illegal to call this method if the current request is not in asynchronous mode (i.e. isAsyncStarted() returns false)",
"param" : null,
"data" : null
}
Code related to the occurrence of an error:
#RestController
#RequestMapping(value = "/cashier/v1")
public class PayController {
#ApiOperation("pay for shop")
#GetMapping(value = "/pay")
public ResponseVO<PayResponseVO> pay(ShopPayRequestVO requestVO, HttpServletRequest request) {
return payService.pay(requestVO, request);
}
}
#Service
#Slf4j
public class PayServiceImpl implements IPayService {
#Override
public ResponseVO<PayResponseVO> pay(ShopPayRequestVO requestVO, HttpServletRequest request) throws AuthorizationException {
log.info("pay request param:{}", JSON.toJSONString(requestVO));
PayResponseVO vo = new PayResponseVO();
......
vo.setOrderNo(businessOrderNo);
vo.setShopId(requestVO.getShopId());
log.info("pay response param:{}", JSON.toJSONString(vo));
return ResponseVO.success(vo);
}
}
#Data
public class PayResponseVO {
private Object payInfo;
private String orderNo;
private String shopId;
}
Single step debugging all code can be executed normally, but the front end still has errors,
I encountered this error, but there is no appropriate solution online. I solved it myself. I share the solution below
The solution to this problem in my project:
PayResponseVO. Java class needs to implement Serializable interface
#Data
public class PayResponseVO implements Serializable {
private static final long serialVersionUID = -7991519952738359328L;
private Object payInfo;
private String orderNo;
private String shopId;
}

Multiple Get Requests with Spring Boot, WebClient and Thymeleaf

I'm trying to make a RESTClient application for consuming the Starwars API.
I take the following URL: https://swapi.py4e.com/api/films/
The user logs in my website and can search for a film (introducing the id in a form).
Later, the website shows title, director, release date and a the list of characters.
For instance, if the user inserts 1 (https://swapi.py4e.com/api/films/1), he should get the following info on screen:
{
"title": "A New Hope",
"episode_id": 4,
"director": "George Lucas",
"characters": [
"https://swapi.py4e.com/api/people/1/",
"https://swapi.py4e.com/api/people/2/",
"https://swapi.py4e.com/api/people/3/",
"https://swapi.py4e.com/api/people/4/",
"https://swapi.py4e.com/api/people/5/",
"https://swapi.py4e.com/api/people/6/",
"https://swapi.py4e.com/api/people/7/",
"https://swapi.py4e.com/api/people/8/",
"https://swapi.py4e.com/api/people/9/",
"https://swapi.py4e.com/api/people/10/",
"https://swapi.py4e.com/api/people/12/",
"https://swapi.py4e.com/api/people/13/",
"https://swapi.py4e.com/api/people/14/",
"https://swapi.py4e.com/api/people/15/",
"https://swapi.py4e.com/api/people/16/",
"https://swapi.py4e.com/api/people/18/",
"https://swapi.py4e.com/api/people/19/",
"https://swapi.py4e.com/api/people/81/"
],
The urls from the characters should show the name of the characters.
This should make, I think, an in-between get request to (for example) the urls and stock them in a list, set or array.
For instance: https://swapi.py4e.com/api/people/1/, I only need the name.
{
"name": "Luke Skywalker",
}
For that, I have the following code snippets (I'm using Spring Boot, MVC and working with layers, as in an enterprise application):
DefaultSWAPIClient (with an interface SWAPIClient):
#Component
class DefaultSWAPIClient implements SWAPIClient {
private final WebClient client;
private final String filmURI;
DefaultSWAPIClient(
WebClient.Builder builder,
#Value("${swapi.films}") String filmURI) {
client = builder.build();
this.filmURI = filmURI;
}
#Override
public Optional<Film> findByEpisodeId(long id) {
try {
return Optional.of(
client.get()
.uri(filmURI, uriBuilder -> uriBuilder.build(id))
.retrieve().bodyToMono(Film.class).block());
} catch (WebClientResponseException.NotFound ex) {
return Optional.empty();
}
}
}
DTO (of DAO) film class:
public class Film {
#JsonProperty("title")
private String title;
#JsonProperty("episode_id")
private long episodeId;
#JsonProperty("director")
private String regisseur;
#JsonProperty("release_date")
private String releaseDate;
#JsonProperty("characters")
private LinkedHashSet<String> characters = new LinkedHashSet<String>();
public String getTitle() {
return title;
}
public long getEpisodeId() {
return episodeId;
}
public String getRegisseur() {
return regisseur;
}
public String getReleaseDate() {
return releaseDate;
}
public LinkedHashSet<String> getCharacters() {
return characters;
}
}
The search film Controller (FilmZoekenController):
#Controller
#RequestMapping("filmzoeken")
class FilmZoekenController {
private final SWAPIClient client;
FilmZoekenController(SWAPIClient client) {
this.client = client;
}
#GetMapping
public ModelAndView toonForm() {
return new ModelAndView("filmzoeken");
}
#GetMapping("/film/{id}")
public ModelAndView getData(#PathVariable long id) {
var modelAndView = new ModelAndView("film");
client.findByEpisodeId(id)
.ifPresent(film -> modelAndView.addObject(film));
return modelAndView;
}
}
Then I show all of it in a thymeleaf template.
I don't understand how to make another GET request to the api before it returns the data.
To sum up: I want to do a get request to films/{id} through a form, get some attributes (one being an array of urls, which need to returns the name value from each direction, so I assume another get request within) and show them to the user.
I hope that this should be enough info/code to help me out! Thanks in advance.

How to make async call inside another async method in Webflux?

Explanation of the question is bit a long. Kindly take a minute and help!
I have two http calls which will give the following data.
1st http request call will return <Mono<List<Chips>>
[
{
"id": 1,
"name": "redlays"
},
{
"id": 2,
"name": "yellowlays"
},
{
"id": 3,
"name": "kurkure"
}
]
Chips Model is
#Data
#Setter
#Getter
#NoArgsConstructor
#AllArgsConstructor
public class Chips {
private int id;
private String name;
}
2nd http request call will return Mono<ChipsDetails> based on Id
{
"id": 1,
"color": "red",
"grams": "50"
}
ChipsDetails Model as below,
#Data
#Setter
#Getter
#NoArgsConstructor
#AllArgsConstructor
public class ChipsDetails {
private int id;
private String color;
private String grams;
}
I have done the Implementation using Webflux. Here I have used three models which are Chips, ChipsDetails and ChipsFullDetails.
Model Chips will have two attributes id and name then Model ChipsDetails will have three attributes id,color and grams whereas Model ChipsFullDetails will have combination of Chips and ChipsDetails attributes which are id, name, color and grams
#RestController
#RequestMapping("/chips")
public class ChipsController {
#Autowired
ChipsService chipsService;
#GetMapping
public Mono<List<ChipsFullDetails>> getAllChips() {
return chipsService.getChips()
.map(f -> {
List<ChipsFullDetails> chipsFullDetails = new ArrayList<>();
f.forEach(a -> {
ChipsFullDetails chipsFullDetail = new ChipsFullDetails();
chipsFullDetail.setId(a.getId());
chipsFullDetail.setName(a.getName());
chipsService.getChipsDetails(a.getId())
.subscribe(b -> {
chipsFullDetail.setColor(b.getColor());
chipsFullDetail.setGrams(b.getGrams());
});
chipsFullDetails.add(chipsFullDetail);
});
return chipsFullDetails;
}
);
}
}
Here chipsService.getChips() will return Mono<List<Chips>> This is the 1st call and chipsService.getChipsDetails(a.getId()) will return Mono<ChipsDetails> This is the 2nd http request call.
The result of the implementation will be ChipsFullDetails
#Data
#Setter
#Getter
#NoArgsConstructor
#AllArgsConstructor
public class ChipsFullDetails {
private int id;
private String name;
private String color;
private String grams;
}
The problem is ChipsFullDetails returns null for color and grams attributes which we are getting from the 2nd http call even though it is subscribed inside.
How to achieve when Second Http call i.e chipsService.getChipsDetails(a.getId()) depending on the result of 1st http call (chipsService.getChips()) in asynchronous way?
Is this possible to achieve without blocking both the calls?
I'd transform the initial Mono<List<Chips>> into a Flux<Chips> first, so that you can flatMap on each element, e.g. something along those lines:
public Mono<List<ChipsFullDetails>> getAllChips() {
return chipsService
.getChips()
// Mono<List> to Flux:
.flatMapIterable(Function.identity())
// flat map on each element:
.flatMap(this::buildChipsFullDetails)
// Flux back to Mono<List>:
.collectList();
}
private Mono<ChipsFullDetails> buildChipsFullDetails(Chips chips) {
return chipsService
.getChipsDetails(chips.getId())
// once you get both chips and details, aggregate:
.map(details -> buildChipsFullDetails(chips, details));
}
private ChipsFullDetails buildChipsFullDetails(Chips chips, ChipsDetails details) {
// straightforward synchronous code:
ChipsFullDetails chipsFullDetail = new ChipsFullDetails();
chipsFullDetail.setId(chips.getId());
chipsFullDetail.setName(chips.getName());
chipsFullDetail.setColor(details.getColor());
chipsFullDetail.setGrams(details.getGrams());
return chipsFullDetail;
}
I basically disagree with the idea of working with a Flux, though I admit I have it as well.
I would say that if you want to get details for a list of chips then you should make an endpoint that does that. Then it will be a single call.
For you original question, there is a way to do it without going to Flux, but it reads a bit funny:
ParameterizedTypeReference<List<Chip>> chipList = new ParameterizedTypeReference<List<Chip>>() {};
public Mono<List<ChipDetails>> getChipDetails() {
return webClient.get().uri("chips").retrieve().bodyToMono(chipList).flatMap(chips -> {
return Mono.zip(chips.stream().map(chip -> webClient.get().uri("chipDetails?id="+chip.getId()).retrieve().bodyToMono(ChipDetails.class)).collect(Collectors.toList()), details -> {
List<ChipDetails> chipDetails = new ArrayList<>();
for (Object o : details) {
chipDetails.add((ChipDetails) o);
}
return chipDetails;
});
});
}
This uses Mono.zip to create a sort of batch request out of each of the Chip entries in the list executes them all at once. Flux will probably end up doing more or less the same thing but not really.
If you just make the endpoint you need, then:
ParameterizedTypeReference<List<ChipDetails>> detailsList = new ParameterizedTypeReference<List<ChipDetails>>() {};
public Mono<List<ChipDetails>> getChipDetailsReal() {
return webClient.post().uri("chipDetails").body(webClient.get().uri("chips").retrieve().bodyToMono(chipList), chipList).retrieve().bodyToMono(detailsList);
}
This approach avoids repeated calls to the same endpoint and is doing what you want.
I'm not a fan of using Flux when you really mean List. A Flux is a streaming thing with backpressure and sophisticated capabilities whereas a List is just a List.

I want to send a List(which is a member of an object) from Postman to Spring REST API

I have an object, and of its attributes is a List. I want to send this object from Postman to my service. I'm using Spring 5.2.7 (Spring MVC, not SpringBoot) and Hibernate 5.4.17 and Java 8. My problem is very similar to this one: I want to send a Postman POST request with an Array: members: ["william", "eric", "ryan"]
This is the class I'm trying to pass in Postman (POST method):
public class ChatDescriptionDto {
private String chatID;
private List<String> members;
private String chatType;
public String getChatID() {
return chatID;
}
public void setChatID(String chatID) {
this.chatID = chatID;
}
public List<String> getMembers() {
return members;
}
public void setMembers(List<String> members) {
this.members = members;
}
public void addMembers(List<String> members)
{
if(this.members == null)
this.members = new ArrayList<>();
this.members.addAll(members);
}
public void addMember(String member)
{
if(this.members == null)
this.members = new ArrayList<>();
this.members.add(member);
}
public String getChatType() {
return chatType;
}
public void setChatType(String chatType) {
this.chatType = chatType;
}
}
I've tried this and it didn't work:
{
"chatID": "123",
"members": ["P2001222833","P2001640916"],
"chatType": "personal"
}
Edit: This is my controller:
#PostMapping("/initiateChat")
public String initiateChat(#RequestBody ChatDescriptionDto chat)
{
return chatServiceLocal.initiateChat(chat)?"Chat Description created":"Failure! Could not save.";
}
Edit 2: The method which I've written in the question, "members": ["P2001222833","P2001640916"], is the correct one. Turns out, there was some error in the server so it never started and I didn't check that.
Having no information about the Controller class you're using, the first thing I'd assume is that you're receiving an empty object, which means that Spring simply skipped the serialization. This is the case when you don't specify the parameter of the method as #RequestBody. First, make sure that you do have the annotation.
#RestController
#RequestMapping("/")
public class TestController {
#RequestMapping(value = "/test", method = RequestMethod.POST)
public ResponseEntity test(#RequestBody ChatDescriptionDto dto) {
System.out.println(dto);
return ResponseEntity.ok().build();
}
}
If that's not the case, I'd assume that the problem is with the content type you're using. Spring uses JSON by default, but you can change it in your endpoint's configuration.
To send a simple object request, you do:
{
"member":"kola"
}
To send a list object request, you do:
{
"member": ["kola","wale","ojo"]
}
This is more like listing array elements.
Any error that pops up after this, is basically not because of the request you sent.

How to unmarshall json lists using Spring Boot RestTemplate

I have to parse a REST response in json and it has a lot of nested lists with many objects.
The response contains an item called "ObjectList" which has a list and inside, two elements, "ObjectA" and "ObjectB". I don't know how to parse the response to objects using Jackson annotations.
The json looks like this:
"ObjectList": [
{
"ObjectA": {
"property1": false,
"property2": true
},
"ObjectB": {
"property1": 66,
"property2": true
},
{
"ObjectA": {
"property1": false,
"property2": true
},
"ObjectB": {
"property1": 66,
"property2": true
}
}
]
}
My code looks like this
ResponseEntity<Response> response = restTemplate.exchange(URL, HttpMethod.GET, request, Response.class);
Response response = response.getBody();
Response is:
#JsonIgnoreProperties(ignoreUnknown = true)
public class TimesheetListResponse {
#JsonProperty("ObjectA")
private List<ObjectA> objectAList;
#JsonProperty("ObjectB")
private List<ObjectB> objectBList;
That does not work at all, and I'm confused about how to map this.
According to your requirement the model structure may look like below. Within the objectList map in Response object, you need to add HashMap with keys as "ObjectA"/"ObjectB" string and value as instance of ObjectA/ObjectB. I have taken value type of Map as Object, so that any object type A/B can fit in there. Add corresponding #JsonXXX annotations.
public class Response {
private List<Map<String,Object>> objectList;
//Getters & Setters
}
public class ObjectB {
String propB1;
String propB2;
}
public class ObjectA {
String propA;
String propA1;
}
I also would consider the entry in the list as another wrapper object that can either ObjectA or ObjectB. I.e.
#JsonIgnoreProperties(ignoreUnknown = true)
public final class Parent {
#JsonProperty("ObjectList")
private List<ChildWrapper> objectList = new ArrayList<>();
}
#JsonIgnoreProperties(ignoreUnknown = true)
public final class ChildWrapper {
#JsonProperty("ObjectA")
private Child ObjectA;
#JsonProperty("ObjectB")
private Child ObjectB;
}
#JsonIgnoreProperties(ignoreUnknown = true)
public final class Child {
#JsonProperty("property1")
private int property1;
#JsonProperty("property2")
private boolean property2;
}
It seems that the mapping was fine, I only had to initialize the Arraylist. The main issue was that the endpoint was returning empty because of a parameter that I forgot.

Categories

Resources