I have a relationship between two objects. I have a Spring CLI communicating with a RESTful Web Service which uses Jackson. I am using the #JsonIdentityInfo annotation, but it is unable to create a relationship between the two classes. Additionally I am using WebClient
The first Object contains the following code:
import com.fasterxml.jackson.annotation.*;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
#JsonSerialize(as= FuncUnit.class)
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "fu_id", scope=FuncUnit.class)
#Setter #Getter
public class FuncUnit {
#JsonProperty(value = "description")
private String description;
#Id #JsonProperty(value = "fu_id", required = true)
private Long fu_id;
// standard constructors
public FuncUnit (long fuId)
{
fu_id = fuId;
}
public FuncUnit() {}
}
The second Object is:
#JsonSerialize(as=Engine.class)
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id", scope = Engine.class)
#Setter #Getter
public class Engine {
#JsonProperty(value = "id", required = true)
private Long id;
#JsonProperty(value = "func_unit")
private FuncUnit func_unit;
public Engine() {}
}
Now the json I receive is the following:
[
{
"id": 111,
"functional_unit": {
"description": "",
"fu_id": 11,
},
},
{
"id": 112,
"functional_unit": 11,
}
]
And the webClient code I wrote is the following. (I also tried without ExchangeStrategies, and I came out with the same result):
ExchangeStrategies exchangeStrategies = ExchangeStrategies.builder()
.codecs(configurer ->
configurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(new ObjectMapper(), MediaType.APPLICATION_JSON)))
.build();
String url = cliUtils.getBaseUrl();
WebClient webClient = WebClient.builder().exchangeStrategies(exchangeStrategies).build();
UriComponentsBuilder builder = UriComponentsBuilder.newInstance().fromHttpUrl(url);
List<Engine> units = webClient.get()
.uri(builder.build().toUri())
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.retrieve()
.bodyToFlux(Engine.class)
.collectList()
.block();
When the webClient code is called, I don't receive any errors, but only one of the Engines has a FuncUnit.
The other Engine contains a null as a FuncUnit.
Short answer: use .bodyToMono(new ParameterizedTypeReference<List>(){}) instead.
Long answer:
I've met exactly the some problem. After a lot of research, none of the solutions work for me. Such as Customized ObjectMapper on the exchangeStrategy.
We know that JsonIdentityInfo serializes the first instance as full object and other same object as references to prevent circular reference issue.
[
{
"id": 111,
"functional_unit": { // <----- First, Fully serialized
"description": "",
"fu_id": 11,
},
},
{
"id": 112,
"functional_unit": 11, // <----- Second, reference Id
}
]
However, this issue is not because of the Serialization/Deserialization but because of the behavior of Flux. Though we use the flux.collectList() to get all the list items, they actually returns one by one (non-blocking) and collects all. The second and following same items won't be able to see the first reference and inject the references. So I try using Mono<List<>> to force them return together and it worked.
Related
I have application in Spring and creating documentation for this in OpenAPI with annotations for controllers methods. For example I have method getById (simplified for readability):
#GetMapping("/{id}")
#ApiResponse(
responseCode = "200",
description = "Successful operation.",
content = #Content(
mediaType = "application/json",
schema = #Schema(implementation = ScheduleResponse.class)
)
)
#ApiResponse(
responseCode = "404",
description = "The object with the specified ID does not exist in the system.",
content = #Content(
mediaType = "application/json",
schema = #Schema(implementation = ApiError.class)
)
)
ScheduleResponse getById(#PathVariable Long id) throws EntityNotFoundException;
For 404 NOT_FOUND I returns my own ApiError with list of ApiErrorDetails interface:
#Getter
public class ApiError {
private final LocalDateTime timestamp;
private final String status;
private final String message;
private List < ApiErrorDetails > details;
}
public interface ApiErrorDetails {
}
In that case, I'm using a specific implementation of the interface:
#Getter
public class EntityNotFoundDetails implements ApiErrorDetails {
private final String field;
private final Object notFoundValue;
}
With the above implementation, I get JSON in the documentation with no specific field information inside details for example:
and for schema:
Instead, I'd like to prepare an example like this:
{
"timestamp": "2021-08-08T13:32:10.875Z",
"status": "string",
"message": "string",
"details": [
{
"field": "string",
"notFoundValue": {}
}
]
}
Of course, I need solution for that specific case. This means that I don't want to add the
#Schema(example = "value")
to the details list because I provide different implementations in different cases.
I found a solution that is not perfect but sufficient for documentation purposes.
All that is needed is to add #Schema annotation with the property oneOf over ApiErrorDetails. For example for two interface implementations: EntityNotFoundDetails and ValidationErrorDetails:
#Schema(oneOf = {EntityNotFoundDetails.class, ValidationErrorDetails.class})
interface ApiErrorDetails {
}
In the documentation, it looks like this:
which suggests a slightly different shape of JSON than in reality, but the schema tab dispels doubts:
Probably the only way to provide one implementation of your choice is to simply use different classes and not extend the interface.
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.
Given a RESTful web service developed using the Spring Boot framework, I wanted a way to suppress the birthDate of all Users in the response. This is what I implemented after looking around for a solution :
#RestController
public class UserResource {
#Autowired
private UserDAOservice userDAOService;
#GetMapping("/users")
public MappingJacksonValue users() {
List<User> users = userDAOService.findAll();
SimpleBeanPropertyFilter filter = SimpleBeanPropertyFilter
.filterOutAllExcept("id", "name");
FilterProvider filters = new SimpleFilterProvider().addFilter(
"UserBirthDateFilter", filter);
MappingJacksonValue mapping = new MappingJacksonValue(users);
mapping.setFilters(filters);
return mapping;
}
}
However, when I hit the rest end point in the browser, I can still see the birth date of the user in the response :
{
"id": 1,
"name": "Adam",
"birthDate": "1980-03-31T16:56:28.926+0000"
}
Question 1 : What API can I use to achieve my objective?
Next, assuming that I want to adhere to HATEOAS in combination with filtering, how can I go about doing this. I am unable to figure out the APIs that can be used for using these two features together :
#GetMapping("/users/{id}")
public EntityModel<User> users(#PathVariable Integer id) {
User user = userDAOService.findById(id);
if (user == null) {
throw new ResourceNotFoundException("id-" + id);
}
EntityModel<User> model = new EntityModel<>(user);
WebMvcLinkBuilder linkTo = linkTo(methodOn(this.getClass()).users());
model.add(linkTo.withRel("all-users"));
//how do I combine EntityModel with filtering?
return model;
}
Question 2 : How do I combine EntityModel with MappingJacksonValue?
Note : I am aware of #JsonIgnore annotation but that would apply the filter for all end points that use the domain; however, I want to restrict the filtering only to the two endpoints above.
Turns out for this to work, I have to add the #JsonFilter annotation above the DTO and provide the same name that was used while creating the SimpleFilterProvider.
#JsonFilter("UserBirthDateFilter")
public class User {
private Integer id;
#Size(min=2, message="user name must be atleast 2 characters")
#ApiModelProperty(notes="user name must be atleast 2 characters")
private String name;
#Past
#ApiModelProperty(notes="birth date cannot be in the past")
private Date birthDate;
//other methods
}
There is an easier way to do this, on your transfer object (the class you are sending back to the client), you can simply use the #JsonIgnore annotation to make sure the field is not serialized, and therefore sent to the client. So simply add #JsonIgnore inside your User class for your birthDay field.
You can also read more here about this approach:
https://www.baeldung.com/jackson-ignore-properties-on-serialization
If you need to return a different object for different endpoints (User without birthDay in your case, only for specific) you should create separate transfer objects and use those for their respective endpoints. You can pass your original entity (User) in the constructor to those classes and copy over all fields needed.
You can use Jackson's #JsonView feature. With this, you can tell a certain request mapping to produce serialized JSON with chosen set of properties.
public class View {
interface UserDetails {}
}
public class User {
#JsonView(View.UserDetails.class)
private Long id;
#JsonView(View.UserDetails.class)
private String name;
private String birthdate;
}
Controller be like
#JsonView(View.UserDetails.class)
#GetMapping("/users")
public MappingJacksonValue users() {
....
}
For question 2, I had the exact same question as you did, and here's what I did. It seems to be working:
#GetMapping(path = "/users/{id}")
public MappingJacksonValue retrieveUser(#PathVariable int id){
User user = service.findOne(id);
if(user==null){
throw new UserNotFoundException("id-"+id);
}
//"all-users", SERVER_PATH + "/users"
EntityModel<User> resource = EntityModel.of(user);
WebMvcLinkBuilder linkTo =
linkTo(methodOn(this.getClass()).retrieveAllUsers());
resource.add(linkTo.withRel("all-users"));
SimpleBeanPropertyFilter filter = SimpleBeanPropertyFilter.filterOutAllExcept("id");
FilterProvider filters = new SimpleFilterProvider().addFilter("UserFilter",filter);
MappingJacksonValue mapping = new MappingJacksonValue(resource);
mapping.setFilters(filters);
return mapping;
}
Response for HTTP GET localhost:8080/users/1
{
"id": 1,
"links": [
{
"rel": "all-users",
"href": "http://localhost:8080/users"
}
]}
I am working in a project where I need to send a request to remote service with 2 different formats
Format 1:
{
"templateId": "template1",
"configurationData": {
"inboundHeaders": [
{
"key": "header1",
"value": "value1"
}, {
"key": "header2",
"value": "value2"
}, {
"key": "header3",
"value": "value3"
}
],
"outboundHeaders": [
{
"key": "header4",
"value": "value4"
}, {
"key": "header5",
"value": "value5"
}, {
"key": "header6",
"value": "value6"
}
]
}
}
Format 2
{
"templateId": "template1",
"configurationData": {
"inboundHeaders": "head1",
"outboundHeaders" : "head2,head3"
}
}
Now I have created one class
#JsonPropertyOrder({ "inboundHeaders", "outboundHeaders"})
public class ConfigurationData {
#JsonProperty("inboundHeaders")
private List<Header> inboundHeaders = null;
#JsonIgnore
#JsonProperty("outboundHeaders")
private List<Header> outboundHeaders = null;
#JsonProperty("inboundHeaders")
private String inboundHeader = null;
#JsonProperty("outboundHeaders")
private String outboundHeader = null;
}
Getters and Setters would go here....
But when I am executing this program. Obviously, I am getting following exception like
com.fasterxml.jackson.databind.JsonMappingException: Multiple fields representing property
How to handle these two different version (java.util.List vs java.lang.String) of requests in one Json POJO?
I think you have two options.
Create two classes and two methods to call remote service like:
Lists:
#JsonPropertyOrder({ "inboundHeaders", "outboundHeaders"})
public class ConfigurationDataLists {
#JsonProperty("inboundHeaders")
private List<Header> inboundHeaders = null;
#JsonIgnore
#JsonProperty("outboundHeaders")
private List<Header> outboundHeaders = null;
}
Strings:
#JsonPropertyOrder({ "inboundHeaders", "outboundHeaders"})
public class ConfigurationDataString {
#JsonProperty("inboundHeaders")
private String inboundHeader = null;
#JsonProperty("outboundHeaders")
private String outboundHeader = null;
}
Use Map
I will prefer option 1.
The answer from Francisco PĂ©rez is absolutely correct but you later clarified your question. The possibilities to limit new classes are - well - limited. You either need to create a class representinng each different DTO or make some sort of manual serializing.
One thing you can do is that you create an interface for different types of configuration data DTOs, so just:
interface IConfigurationData {}
then you have this template create or change it so that configurationData is of type tha interface:
#Getter #Setter
public class Template {
private String templateId;
private IConfigurationData configurationData;
}
Then using the DTO classes in above mentioned answer let them implement this interface, like:
public class ConfigurationDataLists implements IConfigurationData {...}
and
public class ConfigurationDataString implements IConfigurationData {...}
Then you will be able to do two different queries like this:
Template template1 = new Template();
template1.setTemplateId("1");
template1.setConfigurationData(new ConfigurationDataLists());
Template template2 = new Template();
template2.setTemplateId("2");
template2.setConfigurationData(new ConfigurationDataString());
You cannot use the same name to different properties like you did. E.g. - inboundHeaders.
You have to change one of the propertyname. In simple words you have to keep the
#JsonProperty
unique.
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.