Java OpenAPI example for interface as a DTO field - java

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.

Related

Swagger: Unable to find model that actually exists

I'm using Spring Boot v2.6.0 to create a simple hosting service.
For Swagger I'm using SpringFox version 3.0.0, and Swagger2.
As I've never used Swagger or OpenAPI inside my Spring projects I'm out of ideas.
I'm having an issue with this method:
#CrossOrigin
#Operation(summary = "Check if a resourcepack exists with the specified id.")
#ApiResponse(responseCode = "200", description = "Resourcepack found", content = {
#Content(mediaType = "application/json", schema = #Schema(implementation = ExistsResponse.class))
})
#ApiResponse(responseCode = "400", description = "Bad Request", content = {
#Content(mediaType = "application/json", schema = #Schema(implementation = ErrorResponse.class))
})
#ApiResponse(responseCode = "403", description = "Blacklisted IP", content = {
#Content(mediaType = "application/json", schema = #Schema(implementation = ErrorResponse.class))
})
#ApiResponse(responseCode = "429", description = "Too Many Requests (rate-limited)", content = {
#Content(mediaType = "application/json", schema = #Schema(implementation = RateLimitResponse.class))
})
#ApiResponse(responseCode = "500", description = "Internal Error (unseen exceptions)", content = {
#Content(mediaType = "application/json", schema = #Schema(implementation = ErrorResponse.class))
})
#GetMapping(value = "/api/exists/{id:.+}", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<ExistsResponse> exists(#Parameter(description = "Id of the resourcepack to be checked") #PathVariable String id, HttpServletRequest request) throws InternalErrorException {
I get the following error when navigating to swagger-ui, for all models except the ExistsResponse one.
2022-08-05 17:29:18.057 ERROR 17372 --- [nio-8080-exec-1] nceModelSpecificationToPropertyConverter : Unable to find a model that matches key ModelKey{qualifiedModelName=ModelName{namespace='net.iceyleagons.resourcehost.responses', name='ErrorResponse'}, viewDiscriminator=null, validationGroupDiscriminators=[], isResponse=true}
2022-08-05 17:29:18.058 ERROR 17372 --- [nio-8080-exec-1] nceModelSpecificationToPropertyConverter : Unable to find a model that matches key ModelKey{qualifiedModelName=ModelName{namespace='net.iceyleagons.resourcehost.responses', name='ErrorResponse'}, viewDiscriminator=null, validationGroupDiscriminators=[], isResponse=true}
2022-08-05 17:29:18.058 ERROR 17372 --- [nio-8080-exec-1] nceModelSpecificationToPropertyConverter : Unable to find a model that matches key ModelKey{qualifiedModelName=ModelName{namespace='net.iceyleagons.resourcehost.responses', name='RateLimitResponse'}, viewDiscriminator=null, validationGroupDiscriminators=[], isResponse=true}
2022-08-05 17:29:18.059 ERROR 17372 --- [nio-8080-exec-1] nceModelSpecificationToPropertyConverter : Unable to find a model that matches key ModelKey{qualifiedModelName=ModelName{namespace='net.iceyleagons.resourcehost.responses', name='ErrorResponse'}, viewDiscriminator=null, validationGroupDiscriminators=[], isResponse=true}
I'm using Lombok on my models. As an example here is my RateLimitResponse class:
#Data
#RequiredArgsConstructor
public class RateLimitResponse {
private final long refill;
private final String error;
public static RateLimitResponse from(RateLimitedException e) {
return new RateLimitResponse(e.getRefill(), e.getMessage());
}
}
As a comparison here's the ExistsResponse:
#Data
#RequiredArgsConstructor
public class ExistsResponse {
private final String downloadUrl;
private final boolean exists;
private final long available;
private final long remainingTokens;
public static ExistsResponse found(String downloadUrl, long available, long remainingTokens) {
return new ExistsResponse(downloadUrl, true, available, remainingTokens);
}
public static ExistsResponse empty(long remainingTokens) {
return new ExistsResponse("", false, -1, remainingTokens);
}
}
I have no idea why this does not affect the ExistsResponse model, as it's constructed in a totally equal way.
There's a few things wrong here with your code here.
multiple #ApiResponse objects not wrapped
According to the swagger documentation for #ApiResponses, the #ApiResponse object must be wrapped in the responses wrapper. The #ApiResponse documentation says "This annotation is not used directly and will not be parsed by Swagger. It should be used within the ApiResponses."
Controller Advice
You should have an #ControllerAdvice class to manage all exceptions. There's a great article on how to do this here, but feel free to find your own.
Method Signature
Your method signature is ResponseEntity<ExistsResponse>. It can't return another response (unless you define it in #ControllerAdvice). Make this generic if possible, like ResponseEntity<?>.
A couple of other notes:
#Data already includes the #RequiredArgsConstructor, so there's no need to add that annotation here.
The #RequiredArgsConstructor makes a contructor consisting of fields marked with #NonNull. In your case, since you don't have fields annotated this way, then your #RequiredArgsConstructor is basically acting the same as a #NoArgsConstructor.
Java automatically creates a NoArgsConstructor at compile time for a class with no constructors. There's no need to declare the #RerquiredArgsConstructor unless you are also declaring an #AllArgsConstructor and/or an #NoArgsConstructor.
#Data does a lot of stuff. If you are only using it as a simple object, maybe consider #Value instead.
As I was searching through the internet I found, that SpringFox's current version doesn't really support #ControllerAdvice (or rather Exceptions) and there's an open issue related to this on GitHub. (https://github.com/springfox/springfox/issues/521)
I was able to fix my problem by using springdoc instead of Springfox.

How to return required parameters as json response from pojo class in spring boot?

what I am trying to do is,
If I take one pojo class like
#Entity
#Table(name = "property_table")
public class Property {
#Id
#Column(name = "property_id")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int propertyId;
#Column(name = "property_name")
private String propertyName;
#Column(name = "property_type")
private String propertyType;
}
In RestController I wrote Two Methods like
#GetMapping(value = "/getProperties", produces = { "application/json",
"application/xml" }, consumes = { "application/xml", "application/json" })
#ResponseBody
public List<Property> getProperties() {
//some code
}
#GetMapping(value = "/getPropertyById", produces = { "application/json",
"application/xml" }, consumes = { "application/xml", "application/json" })
#ResponseBody
public Property getPropertyById() {
//some code
}
So, hear what I am trying to do is
for first api method I want return json like some parameters from Property pojo class i.e., like
for getProperties api method
{
"property":[
{
"propertyId":001,
"propertyName":"PROPERTY 1"
},
{
"propertyId":002,
"propertyName":"PROPERTY 2"
}
],
In the Above json I want to return only two parameters i.e propertyId,propertyName and remaining parameter i.e propertyType I dont want to retun in json.
How to return like that?
and for the second api method I want to return all three parameters. i.e., like below
for getPropertyById api method
{
"propertyId":001,
"propertyName":"PROPERTY 1",
"propertyType:"PROPERTY_TYPE 1"
},
how to maintain different json response using same pojo class with different parameters for different api methods.
please help me to solve this isuue.
Thanks.
REST API under/over-fetching is a well-known problem. There's only two (classical ways) to handle that.
The first one is to build one model per each attribute visibility state. So, in your case, you'll need to create two different models (this kind of models are called DTO - Data Transfert Object). One model will have a propertyType attribute, the other will not. The model Property you've shared shows that you use the same class as entity and as transfert object. This solution will add some complexity to your app because you will have to implement some mappers to convert your entity to a corresponding DTO.
The second one is to accept that you send an attribute that will not be useful (be aware of the over-fetching). This solution is often the most adopted one. The cons of this solution is when you don't want to send something to your client (imagine a User model, you want to get the password from your client but you don't want to sent it back to it). Another obvious negative point is that the transactions will be larger but it is negligible in most cases
I would strongly advice you to keep your #Entity isolated in the 'db' layer. So that changes on the database side don't affect your API and vice versa. Also, you will have much better control over what data is exposed in your API. For your needs you can create 2 true DTOs, like PropertyDto and PropertyDetailsDto (or using private fields and getters/setters).
public class PropertyDto {
public String propertyId;
public String propertyName;
}
public class PropertyDetailsDto extends PropertyDto {
public String propertyType;
}
Map your #Entity to a specific dto corresponding to your needs.
EDIT
public List<PropertyDto> getProperties() {
return toPropertyDtos(repository.findAll());
}
public PropertyDetailsDto getPropertyById(Long id) {
return toPropertyDetailsDto(repository.findBy(id));
}
in some Mapper.java
...
public static List<PropertyDto> toPropertyDtos(List<Property> properties) {
return properties.stream()
.map(Mapper::toPropertyDto)
.collect(toList());
}
private static PropertyDto toPropertyDto(Property property) {
PropertyDto dto = new PropertyDto();
dto.propertyId = property.propertyId;
dto.propertyName = property.propertyName;
return dto;
}
// same stuff for `toPropertyDetailsDto`, you could extract common mapping parts in a separate private method inside `Mapper`
...

Unable to get JsonIdentityInfo to work correctly

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.

How can I change the request body in Swagger 1.5 to an example JSON request body

Current look of Swagger request
Is it possible to change the body
{
"config": {
"additionalProp1": {},
"additionalProp2": {},
"additionalProp3": {}
},
"class": "string"
}
to look like
{
"class": "my.class.com",
"config": {
"myParam": "",
"datacenter": "USA",
}
Currently the method looks like this
#POST
#Path("/run")
#Consumes(MediaType.APPLICATION_JSON)
#ApiOperation(value = "Runs a specified job asynchronously",
notes = "Doesn't work for full tasks.")
public Response createTask(TaskConfig taskConfig) {
TaskConfig is a basic class with two member variables
public class TaskConfig {
#JsonProperty("class")
#NotNull
private String clazz;
private Map<String, Object> config;
We are using Swagger 1.5 from this Dropwizard Swagger bundle library. I know that 2.0 has the #RequestBody annotation and I just want to make sure that is my only option before I go down the path of upgrading.
Even by using #RequestBody we need to add additional annotation within TaskConfig class.
We are using Swagger 2.0.x. The TaskConfig class would have fields something like this :
#Schema(
description = " My descriptions",
type = "array",
example = " {\"myParam\" :\"value\" ,"
+ "\"datacenter\": \"USA\"}")
private Map<String, Object> config;
Similarly clazz can also be annotated like :
#Schema(description = "The field descrition", example = "true")
And RequestBody annotation will look like :
#RequestBody(
description = "Description of TaskConfig ",
content = #Content(schema = #Schema(implementation = TaskConfig .class)))
I got the result as shown in the image:

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