How to customize the API metadata of springdoc-openai? - java

I'm trying to customize the springdoc-openapi, make it can work with my framework, but I meet two problems.
1. How to treat methods that do not start with is/get as properties of Model?
If users use my ORM framework by Java language, the property getters in the entity interface can either start with is/get like a traditional Java Bean, or don't start with is/get like a Java record, for example
#Entity
public interface Book {
#Id
long id();
String name();
int edition();
BigDecimal price();
#ManyToOne
BookStore store();
#ManyToMany
List<Author> authors();
}
Here, the wording that does not start with is/get is used, which looks like a java record, not a traditional java bean.
However, doing this will cause swagger-ui to think that the model doesn't have any attributes. So I have to change the behavior of swagger.
After some research, I found that this behavior can be changed using io.swagger.v3.core.converter.ModelConverter, which is the most likely solution.
However, springdoc-openapi does not explain in detail how to use ModelConverter in the documentation. Ultimately, this goal was not achieved.
2. How to control the shape of dynamic objects in HTTP response?
My ORM is GraphQL-style, its entity objects are dynamic so that data structures of arbitrary shapes can be queried, just like GraphQL does. For example
#RestController
public class BookController {
#AutoWired
private JSqlClient sqlClient;
// Query simple book objects
#GetMapping("/books")
public List<Book> books() {
return sqlClient.getEntities().findAll(Book.class);
}
// Query complex book objects
#GetMapping("/books/details")
public List<Book> bookDetails() {
return sqlClient.getEntities().findAll(
// Like the request body of GraphQL
BookFetcher$
.allScalarFields()
.store(
BookStoreFetcher.$.allScalarFields()
)
.authors(
AuthorFetcher.$.allScalars()
)
);
}
}
The first query returns a list of simple book objects in the format {id, name, edition, price}
The second query returns a list of complex book objects in the format {id, name, edition, price, store: {id, name, website}, authors: {id, firstName, lastName, gender}}
Dynamic objects can vary in shape, and these are just two special cases.
I expect swgger to tell the client the shape of the object returned by each business scenario. So, I defined an annotation called #FetchBy. It should be used like this
#RestController
public class BookController {
private static final Fetcher<Book> BOOK_DETAIL_FETCHER =
BookFetcher$
.allScalarFields()
.store(
BookStoreFetcher.$.allScalarFields()
)
.authors(
AuthorFetcher.$.allScalars()
);
#AutoWired
private JSqlClient sqlClient;
#GetMapping("/books")
public List<Book> books() {
return sqlClient.getEntities().findAll(Book.class);
}
#GetMapping("/books/details")
public List<#FetchBy("BOOK_DETAIL_FETCHER") Book> bookDetails() {
return sqlClient.getEntities().findAll(BOOK_DETAIL_FETCHER);
}
}
Declare the shape of the complex object as a static constant.
The #FetchBy annotation uses the constant name to tell swgger the shape of the returned dynamic object.
After some research, I found that this behavior can be changed using org.springdoc.core.customizers.OperationCustomizer, which is the most likely solution.
However, I found that the schema tree of swagger is not consistent with the generic type definition tree in the java language. For example, Spring's ResponseEntity<> wrapper will be ignored by swagger and will be not parsed as a node of schema tree. Theoretically speaking, this ability of swagger can be customized infinitely, so the two trees may not always be consistent and difficult to analyze.

Related

Spring Pageable Sort change name of property

Is there a way to map properties of Sort objects? Just a little example:
a Dto
public class OrderDto {
private long totalAmount;
private long openAmount;
}
and an entity
#Entity
public class Order {
private long amount;
}
and a controller
#RestController
public class OrderController {
#GetMapping("/orders")
public Page<OrderDto> findOrders(Pageable pageable) {
// Do something
}
}
I would now call /orders?sort=totalAmount,desc
Is it possible to map the sort property, in this case the totalAmount from the Dto to only amount to use this for a generated JPA query in a charmant way?
I don't know exactly if I get your question right, but...
Do I understand you correct, that you want to map the request-parameter sort with the value totalAmount,desc to be available through the Sort object of the given Pageable object as amount (with order desc)?
Because of the names you mention in your post, I expect that you have Spring Data on the classpath.
IDKFS but maybe class SortHandlerMethodArgumentResolverCustomizer could help you to customize the SortHandlerMethodArgumentResolver the way, that you can map the request-parameter like you want to have.
If not, it should be possible to provide a #Bean of type SortHandlerMethodArgumentResolver that implements the behavior you want to have.
I'm not completely sure if the framework allows to override the specific part you need to be customized but it is IMO worth to have a look. :)

Should i use model classes or payload classes to serialize a json response

I'm using spring boot with mysql to create a Restful API. Here's an exemple of how i return a json response.
first i have a model:
#Entity
public class Movie extends DateAudit {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Date releaseDate;
private Time runtime;
private Float rating;
private String storyline;
private String poster;
private String rated;
#OneToMany(mappedBy = "movie", cascade = CascadeType.ALL, orphanRemoval = true)
private List<MovieMedia> movieMedia = new ArrayList<>();
#OneToMany(mappedBy = "movie", cascade = CascadeType.ALL, orphanRemoval = true)
private List<MovieReview> movieReviews = new ArrayList<>();
#OneToMany(mappedBy = "movie", cascade = CascadeType.ALL, orphanRemoval = true)
private List<MovieCelebrity> movieCelebrities = new ArrayList<>();
// Setters & Getters
}
and correspond repository:
#Repository
public interface MovieRepository extends JpaRepository<Movie, Long> {
}
Also i have a payload class MovieResponse which represent a movie instead of Movie model, and that's for example if i need extra fields or i need to return specific fields.
public class MovieResponse {
private Long id;
private String name;
private Date releaseDate;
private Time runtime;
private Float rating;
private String storyline;
private String poster;
private String rated;
private List<MovieCelebrityResponse> cast = new ArrayList<>();
private List<MovieCelebrityResponse> writers = new ArrayList<>();
private List<MovieCelebrityResponse> directors = new ArrayList<>();
// Constructors, getters and setters
public void setCelebrityRoles(List<MovieCelebrityResponse> movieCelebrities) {
this.setCast(movieCelebrities.stream().filter(movieCelebrity -> movieCelebrity.getRole().equals(CelebrityRole.ACTOR)).collect(Collectors.toList()));
this.setDirectors(movieCelebrities.stream().filter(movieCelebrity -> movieCelebrity.getRole().equals(CelebrityRole.DIRECTOR)).collect(Collectors.toList()));
this.setWriters(movieCelebrities.stream().filter(movieCelebrity -> movieCelebrity.getRole().equals(CelebrityRole.WRITER)).collect(Collectors.toList()));
}
}
As you can see i divide the movieCelebrities list into 3 lists(cast, directos and writers)
And to map a Movie to MovieResponse I'm using ModelMapper class:
public class ModelMapper {
public static MovieResponse mapMovieToMovieResponse(Movie movie) {
// Create a new MovieResponse and Assign the Movie data to MovieResponse
MovieResponse movieResponse = new MovieResponse(movie.getId(), movie.getName(), movie.getReleaseDate(),
movie.getRuntime(),movie.getRating(), movie.getStoryline(), movie.getPoster(), movie.getRated());
// Get MovieCelebrities for current Movie
List<MovieCelebrityResponse> movieCelebrityResponses = movie.getMovieCelebrities().stream().map(movieCelebrity -> {
// Get Celebrity for current MovieCelebrities
CelebrityResponse celebrityResponse = new CelebrityResponse(movieCelebrity.getCelebrity().getId(),
movieCelebrity.getCelebrity().getName(), movieCelebrity.getCelebrity().getPicture(),
movieCelebrity.getCelebrity().getDateOfBirth(), movieCelebrity.getCelebrity().getBiography(), null);
return new MovieCelebrityResponse(movieCelebrity.getId(), movieCelebrity.getRole(),movieCelebrity.getCharacterName(), null, celebrityResponse);
}).collect(Collectors.toList());
// Assign movieCelebrityResponse to movieResponse
movieResponse.setCelebrityRoles(movieCelebrityResponses);
return movieResponse;
}
}
and finally here's my MovieService service which i call in the controller:
#Service
public class MovieServiceImpl implements MovieService {
private MovieRepository movieRepository;
#Autowired
public void setMovieRepository(MovieRepository movieRepository) {
this.movieRepository = movieRepository;
}
public PagedResponse<MovieResponse> getAllMovies(Pageable pageable) {
Page<Movie> movies = movieRepository.findAll(pageable);
if(movies.getNumberOfElements() == 0) {
return new PagedResponse<>(Collections.emptyList(), movies.getNumber(),
movies.getSize(), movies.getTotalElements(), movies.getTotalPages(), movies.isLast());
}
List<MovieResponse> movieResponses = movies.map(ModelMapper::mapMovieToMovieResponse).getContent();
return new PagedResponse<>(movieResponses, movies.getNumber(),
movies.getSize(), movies.getTotalElements(), movies.getTotalPages(), movies.isLast());
}
}
So the question here: is it fine to use for each model i have a payload class for the json serialize ? or it there a better way.
also guys id it's there anything wrong about my code feel free to comment.
I had this dilemma not so long back, this was my thought process. I have it here https://stackoverflow.com/questions/44572188/microservices-restful-api-dtos-or-not
The Pros of Just exposing Domain Objects
The less code you write, the less bugs you produce.
despite of having extensive (arguable) test cases in our code base, I have came across bugs due to missed/wrong copying of fields from domain to DTO or viceversa.
Maintainability - Less boiler plate code.
If I have to add a new attribute, I don't have to add in Domain, DTO, Mapper and the testcases, of course. Don't tell me that this can be achieved using a reflection beanCopy utils like dozer or mapStruct, it defeats the whole purpose.
Lombok, Groovy, Kotlin I know, but it will save me only getter setter headache.
DRY
Performance
I know this falls under the category of "premature performance optimization is the root of all evil". But still this will save some CPU cycles for not having to create (and later garbage collect) one more Object (at the very least) per request
Cons
DTOs will give you more flexibility in the long run
If only I ever need that flexibility. At least, whatever I came across so far are CRUD operations over http which I can manage using couple of #JsonIgnores. Or if there is one or two fields that needs a transformation which cannot be done using Jackson Annotation, As I said earlier, I can write custom logic to handle just that.
Domain Objects getting bloated with Annotations.
This is a valid concern. If I use JPA or MyBatis as my persistent framework, domain object might have those annotations, then there will be Jackson annotations too. If you are using Spring boot you can get away by using application-wide properties like mybatis.configuration.map-underscore-to-camel-case: true , spring.jackson.property-naming-strategy: SNAKE_CASE
Short story, at least in my case, cons didn't outweigh the pros, so it did not make any sense to repeat myself by having a new POJO as DTO. Less code, less chances of bugs. So, went ahead with exposing the Domain object and not having a separate "view" object.
Disclaimer: This may or may not be applicable in your use case. This observation is per my usecase (basically a CRUD api having 15ish endpoints)
We should each layer separate from other. As in your case, you have defined the entity and response classes. This is right way to separate things, we should never send the entity in the response. Even for request thing we should have a class.
What the issue if we are sending entity instead of response dto.
Not available to modify them because we already expose it with our client
Sometimes we don't want to serialize some fields and send as response.
Some overhead are there to translate request to domain, entity to domain etc. But its okay to keep more organized. ModelMapper is the best choice for translation purpose.
Try to use construct injection instead of setter for mandate dependency.
It is always recommended to separate DTO and Entity.
Entity should interact with DB/ORM and DTO should interact with client layer(Layer for request and response) even if the structure of Entity and DTO same.
Here Entity is Movie and
DTO is MovieResponse
Use your existing class MovieResponse for request & response.
Never use Movie class for request & response.
and the class MovieServiceImpl should contain business logic for converting Entity to DTO, Or you can use Dozer api to do auto conversion.
The reason for sepating:
In case you need to add/remove new elements in Request/response you dont have to change much code
if 2 entity have 2 way mapping(e.g. one-to-many/many-to-many relationship) then
JSON object cant be created if object have nested data, this will throw error while serializing
if Anything changed in DB or Entity, then this will not affect JSON Response(most of the time).
Code will be clear and easy to maintain.
On one side you should separate them because sometimes some of the JPA annotations which you use in your model don't work well with the json processor annotations. And yes, you should keep the things separated.
What if you later decide to change your data layer? Will you have to rewrite all your client side?
On the other side, there is this problem of mapping. For that, you can use a library with a small performance penalty.
DTO is a design pattern and solves the problem of fetching as maximum useful data from a service as possible.
In case of a simple application as yours, the DTOs tend to be similar to the Entity classes. However for certain complex applications, DTOs can be extended to combine data from various entities to avoid multiple requests to the server and thus save valuable resources and request-response time.
I would suggest not to duplicate the code in a simple case like this and use model classes in response to the APIs as well. Using separate response classes as DTOs will not solve any purpose and will only make maintaining the code difficult.
While most people have answered pros and cons of using DTO objects, I would like to give my 2 cents. In my case DTO was necessary because not all fields persisted in database were captured from user. There were a few fields which were computed based on user input(of other fields) and were not exposed to users. Also, it can also reduces the size of payload which could result in better performance in such cases.
I advocate for separating the "Payload" or "Data" object from the "Model" or "Display" object. Pretty much always. This just keeps things easier to manage.
Here's an example:
Let's say you need to hit an API that gives you data about cats for sale. Then you parse the data into a cat model object and populate a list of cats that is then displayed to the user. Cool.
But now you want to integrate another API and pull cats from 2 databases. But you run into a problem. One API returns furColor for the color and the new one returns catColor for the color.
If you were using the same object to also display the info, you have some options:
Add both furColor and catColor to the model object, make them both optional, and do some kind of computed property to check which one is set and use that one to display the color
In reality, this is rarely an option because the responses will usually be much more different than just one value like this so you would likelly need a whole new parser anyway
Add a new data object and then also a new adapter and then have to do some kind of check to know which adapter to use when
Something else that still isn't pretty or fun to work with
However, if you create a data object that catches the response, and then a display object that has only the info needed to populate the list, this becomes really easy:
You have a data object that captures the response from the first API
Now make a data object that captures the response from the second API
Now all you need is some kind of simple mapper to map the response to the Display Object
Now both will be converted to a common simple display object, and the same adapter can be used to display the new cats without additional work
This also will make storing the data locally much cleaner.

How to cast Jest SearchResult hits to domain object

I am using Jest to query Elasticsearch and so far it has been great. Jest's documentation says:
Result can be cast to List of domain object;
... and shows this example:
SearchResult result = client.execute(search);
List<SearchResult.Hit<Article, Void>> hits = searchResult.getHits(Article.class);
// or
List<Article> articles = result.getSourceAsObjectList(Article.class);
getSourceAsObjectList is deprecated, and I am using:
List<SearchResult.Hit<ImmutableConceptDocument, Void>> concepts = result.getHits(ImmutableConceptDocument.class);
... Where ImmutableConceptDocument is an immutables generated class - otherwise pretty straight forward POJO, with attributes named as I see under the source of my search results.
However, when I use the above line, I don't get the source properties mapped, I do get other details like score, type, index etc. mapped.
What am I missing? Does the domain class need to have specific Jest annotations or something like that?
I can't see any good examples in the unit tests too. This one maps to Object.class and that does not show me a mapping example.
Here is the immutable class:
#Value.Immutable
public abstract class EsConceptDocument {
public abstract String term();
public abstract Category type();
public abstract List<String> synonyms();
}
... where Category is an enum type.
As Val pointed out in the comments, this was because immutables.io makes the generated class' constructor private (and exposes a builder).
I removed immutable from this class and wrote a constructor and getters and it worked.

Is it ok to pass interface of DTO to DAO

It's about passing interface of DTO to DAO.
For example I have following code
public interface User {
String getName();
}
public class SimpleUser implements User {
protected String name;
public SimpleUser(String name) {
this.name = name;
}
#Override
public String getName() {
return name;
}
}
// Mapped by Hibernate
public class PersistentUser extends SimpleUser {
private Long id;
// Constructor
// Getters for id and name
// Setters for id and name
}
I'm using generic DAO. Is it ok if I create DAO with using interface User instead PersistentUser?
User user = new PersistentUser(name);
UserDao.create(user);
I read a lot of topics on stack but not figured out is this approach ok or no. Please help me. Maybe this is stupid and I can achive only problems.
About separating beans.
I did this because some classes I want to share via API module, that can be used outside to create entities and pass them to my application. Because they uses interface I developed so I can pass them to my DAO for persisting.
Generally, I would say it is ok, but there are a few hidden problems. A developer could cast the object down or access some state via a toString method that shouldn't be accessible. If you don't be careful, it could happen that state is serialized as JSON/XML in webservices that shouldn't be serialized. The list goes on.
I created Blaze-Persistence Entity Views for exactly that use case. You essentially define DTOs for JPA entities as interfaces and apply them on a query. It supports mapping nested DTOs, collection etc., essentially everything you'd expect and on top of that, it will improve your query performance as it will generate queries fetching just the data that you actually require for the DTOs.
The entity views for your example could look like this
#EntityView(PersistentUser.class)
interface User {
String getName();
}
Querying could look like this
List<User> dtos = entityViewManager.applySetting(
EntityViewSetting.create(User.class),
criteriaBuilderFactory.create(em, PersistentUser.class)
).getResultList();

Is it possible to write web-service that returns a collection of generic type? Spring 3

In my db I have a number of entity classes and I run standart CRUD operations on them via Hibernate. Its not a problem to create generic dao class to make all main operations with classes. For example, in dao I have methods which look like this:
<T> List<T> loadAll(Class clazz)
Now I want to expose these methods to web-service client via Spring 3 operated web-service.
The only way I see is to implement web-methods for all entities i.e. write a class that looks like...
class BookResponse { List<BookEntity> books; }
... and return this in corresponding web-method "BookResponse getAllBooks()". This will ruin my attemts to make a code simplier by using dao with generics.
Is there are any other ways?
How can I do this without implementing web-methods for ALL my entities?
If generic web-service is not possible may be there are some other ways to resolve this task in a simple way?
UPDATE:
At the moment I am trying to implement a response class which should look like
public class ServiceResponse<T>{
#XmlElementWrapper( name = "data" )
#XmlElements( #XmlElement(name = "a", type = EntityA.class), #XmlElement(name = "b", type = EntityB.class) )
private List<T> data = new ArrayList<T>( );
//getters,setters
}
So I want to be able to insert a list of any entities mapped with annotations to this response. This produces no erros, but the response given me by web-service is empty.
I think you'll need a new POJO "GenericEntity" which can hold the information of any domain entity class instance.
It would hold a type string and an arbitrary/generic list of named attributes.
It can then be used to represent any of your real domain entities
e.g.
type = Book
attributes = (title=Order of the Phoenix, author=J K Rowling)
e.g.
type = Car
attributes = (make=Renault, model=Clio)
These examples show String attributes so you'll have to sort out if this is good enough or if you need strong typing - it's possible but harder.
You can then expose your "GenericEntity" via web services, allowing clients to make calls in and specify which domain entity they wish to search for, and even allow them to specify search criteria too.
Adds and deletes could be done in a similar way.
HTH,
David

Categories

Resources