Issue in rendering Swagger OpenAPI v3 UI + Springboot project - swagger

I am getting the error -
"Please indicate a valid Swagger or OpenAPI version field. Supported version fields are swagger: "2.0" and those that match openapi: 3.0.n (for example, openapi: 3.0.0)."
when I paste the generated json from /api-docs to editor.swagger.io.
I have some observations.
localhost:8080/v3/api-docs/ yields -
"{\"openapi\":\"3.0.1\", "\info\":{ ----------------
This json gives the mentioned error in editor.swagger.io.
On manually removing the initial double quotes and escape character i.e.
{"openapi":"3.0.1", "info":{ ---------------- the error goes away i.e. UI is rendered without any issue.
My project springboot version is 2.2.13.RELEASE, springdoc-openapi-ui version is 1.5.8, jackson-databind version is 2.10.5.1
OpenAPI config class -
#Configuration
public class Config1 {
#Bean
public GroupedOpenApi publicApi() {
return GroupedOpenApi.builder()
.group("user")
.pathsToMatch("/v1/**")
.build();
}
#Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info().title("title1").description("test").version("1.0.0"));
}
Please let me know if any further information is required from my end. Any inputs regarding the issue will be helpful. Thank you.

I had the same issue with spring-boot-2.6.4, springdoc-openapi-1.6.6.
The problem is localhost:8080/v3/api-docs/ responses "{"openapi":"3.0.1", "\info":{..." which is a String, NOT a valid JSON as expected.
It turns out in my WebMvcConfig implements WebMvcConfigurer, it doesn't have a StringHttpMessageConverter, so the solution is to add it to converters:
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(stringHttpMessageConverter());
...
}
#Bean
public StringHttpMessageConverter stringHttpMessageConverter() {
StringHttpMessageConverter messageConverter = new StringHttpMessageConverter();
messageConverter.setSupportedMediaTypes(List.of(MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN, MediaType.ALL));
return messageConverter;
}
If you're extending WebMvcConfigurationSupport instead of implementing WebMvcConfigurer, probably you'll not get this error as the StringHttpMessageConverter is added by default.

You can use .version("v0.0.1") in your customOpenAPI or publicApi like this
#Bean
public OpenAPI springShopOpenAPI() {
return new OpenAPI()
.version("v0.0.1"));
}
For more details see docs https://springdoc.org/#migrating-from-springfox

Related

How to configure correct the swagger OpenAPI (wrong url generated)?

I made a small spring boot application (2.2.5.RELEASE)
and I want to have swagger and I use springdoc-openapi-ui version 1.6.8.
I have in my application.properties my setting "swagger-ui.hostname" which is an empty string by default.
Other settings I have for springdoc are
springdoc.api-docs.path=/api-docs
springdoc.swagger-ui.use-root-path=false
springdoc.swagger-ui.operationsSorter=method
springdoc.swagger-ui.enabled=true
springdoc.api-docs.enabled=true
I can run locally the application and execute the GET endpoint test.
I can see
http://localhost:8089/swagger-ui.html
( "swagger-ui.hostname" is an empty string )
I have one problem when I do deploy on tomcat server and I use
"-Dswagger-ui.hostname=company.com-live".
The problem is swagger or openapi generates url wrong for curl in the page swagger-ui.html when I execute one endpoint test (get).
I can see in the page of swagger
https://company.com-live/swagger-ui.html
curl -X 'GET' \
'https://company.com-live/api-docs/company.com-live/test' \
-H 'accept: */*'
The app was started with
"-Dswagger-ui.hostname=https://company.com-live"
( "company.com-live" is duplicated and "/api-docs" is added )
I expected this url
https://company.com-live/test
These works
https://company.com-live/internal/mon/info
https://company.com-live/ping
How to configure correct the swagger OpenAPI to obtain this url ?
https://company.com-live/test
Java code for configuration is
#Configuration
#Slf4j
public class SwaggerConfig {
#Value("${swagger-ui.hostname:}")
private String hostname;
#Bean
public OpenAPI springShopOpenAPI() {
List<Server> serversList = new ArrayList<>();
System.out.println(message);
log.info(message);
if (StringUtils.isNoneBlank(hostname)) {
serversList.add(new Server().url(hostname));
}
return new OpenAPI()
.components(new Components()
.addSecuritySchemes("basicScheme",
new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("basic")))
.info(new Info()
.title("MyTools")
.description("MyTools")
.version("1.0.0")
.contact(new Contact()
.email("it-operations#company.com")
.name("Company support"))
.license(new License()
.name("Company")
.url("https://company.com")))
.servers(serversList);
}
#Bean
public GroupedOpenApi addressesApi() {
return GroupedOpenApi.builder()
.group("Test group")
.pathsToMatch("/test/**")
.build();
}
#Bean
public GroupedOpenApi pingApi() {
return GroupedOpenApi.builder()
.group("Ping")
.pathsToMatch("/ping/**")
.build();
}
}

Deprecated APIs should be marked accordingly in the Swagger UI

Feature Request
Today it is possible to version APIs and most things around that work perfectly fine with Swagger. What I am really missing here is the possibility to make it transparent for any Swagger UI users that an API version has been marked as deprecated.
API deprecation in aspnetcore is described here.
My expectation would be to have an icon or a tag which says "OBSOLETE" or "DEPRECATED" next to the API group name.
On a side note:
The Swashbuckle Swagger ASPNET.Core github project issue tracker advised to open feature requests on SO.
Edit:
The whole Controller is marked as deprecated using the ApiVersion attribute. If you mark the controller as [Obsolete] all the methods are grey and text is striked through. However this is not what I am looking for. I don't want mark my codebase [Obsolete]. I want to mark a specific API version as deprecated so people know they should switch to a newer version.
[ApiVersion("1", Deprecated = true)]
[Route("v{version:apiVersion}/[controller]")]
[Authorize("my.auth.policy")]
[ApiController]
public class MyApiController
{
// do stuff
}
My current workaround is this:
In my Startup I add the swaggerUI and do a custom formatting on the swagger endpoint dropdown display.
app.UseSwagger();
app.UseSwaggerUI(options =>
{
foreach (ApiVersionDescription apiVersionDescription in apiVersionDescriptionProvider.ApiVersionDescriptions.OrderByDescending(a => a.ApiVersion))
{
string isDeprecated = apiVersionDescription.IsDeprecated ? " (DEPRECATED)" : string.Empty;
options.SwaggerEndpoint($"{Configuration["PathBase"]}/swagger/{apiVersionDescription.GroupName}/swagger.json",
$"{apiVersionDescription.GroupName.ToUpperInvariant()}{isDeprecated}");
}
});
#helen
Definition
public class CustomHeaderFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
if (operation.Parameters == null)
operation.Parameters = new List<OpenApiParameter>();
var apiDescription = context.ApiDescription;
if (apiDescription.IsDeprecated())
{
operation.Deprecated = true;
}
}
}
Use
builder.Services.AddSwaggerGen(g =>
{
g.OperationFilter<CustomHeaderFilter>();
}).AddSwaggerGenNewtonsoftSupport();
The interface must have the following
[ApiVersion("1.0", Deprecated = true)]
public class WeatherForecastController : ControllerBase
result

"No KeyInfo Generator provided" for spring-security-saml2-service-provider

I have an application that connects to a SAML idP that only supports the POST Binding. After configuring my application which uses spring-security-saml2-service-provider to manually create a POST Authentication request, I looked at the XML that got generated and saw that it included the Signature information (which is expected) but not the Key Info. Then in the logs, I noticed it said:
No KeyInfoGenerator was supplied in parameters or resolveable for credential type org.opensaml.security.x509.X509Credential, No KeyInfo will be generated for Signature
This is what my code looks like to manually generate the POST Authentication request:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic()
.disable()
.csrf()
.disable();
}
#Bean
public RelyingPartyRegistration nnanetRelyingPartyRegistration() {
SAMLMetadataSignatureSigningParametersResolver resolver = new SAMLMetadataSignatureSigningParametersResolver();
return RelyingPartyRegistrations
.fromMetadataLocation("https://example.com/metadata.xml")
.entityId("example")
.registrationId("nnanet")
.assertingPartyDetails(party -> {
party.wantAuthnRequestsSigned(true)
.singleSignOnServiceLocation("https://example.com/login")
.entityId("https://example.com/login");
.verificationX509Credentials(saml2X509Credentials -> {
saml2X509Credentials.add(getVerificationCertificate());
});
})
.signingX509Credentials(saml2X509Credentials -> {
saml2X509Credentials.add(getSigningCredential());
})
.decryptionX509Credentials(saml2X509Credentials -> {
saml2X509Credentials.add(getSigningCredential());
})
.build();
}
#Bean
public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() {
return new InMemoryRelyingPartyRegistrationRepository(relyingPartyRegistration());
}
#Bean
public Saml2PostAuthenticationRequest saml2PostAuthenticationRequest() {
Saml2AuthenticationRequestContext.Builder contextBuilder = Saml2AuthenticationRequestContext.builder();
contextBuilder.assertionConsumerServiceUrl("http://localhost:8080/saml/SSO");
contextBuilder.relyingPartyRegistration(relyingPartyRegistration());
contextBuilder.issuer("issuer");
OpenSamlAuthenticationRequestFactory factory = new OpenSamlAuthenticationRequestFactory();
return factory.createPostAuthenticationRequest(contextBuilder.build());
}
I just call the saml2PostAuthenticationRequest() method from my Controller and generate a form to submit automatically due to some reasons that are outside the scope of this question. After looking further into the OpenSamlAuthenticationRequestFactory, it looks like this is creating the SignatureSigningParameters specifically without including the KeyInfoGenerator as it's only being created in the private methods. Does anyone have an idea on how to get around this, or perhaps point out if I'm doing something wrong?
Thanks!
This will be addressed in a future version of Spring Security SAML 2
Please review the PR - https://github.com/spring-projects/spring-security/pull/9746

How can I document objects that I don't have ownership over?

I'm returning a Page<SomeObject> In my method endpoints. In my OpenAPI UI, how can I generate a description of the fields that come from org.springframework.data.domain.Page type.
Normally what I do is go to the Object that I want to document and annotate its fields with #Schema(description = "some description"). But I don't have access to the Page class. How can I document its fields?
I'm probably missing something simple here, but any help would be appreciated.
PS: I am using OpenAPI v3 and springdoc
You can use OpenApiCustomiser.
Let's say for example you have this controller:
#RestController
public class HelloController {
#GetMapping("/hello")
Page<PersonDTO> getPage() {
return null;
}
}
Let's suppose you want add the description the Pageable element:
#Bean
public OpenApiCustomiser pageableOpenApiCustomiser() {
return openApi -> {
Schema pageableSchema = openApi.getComponents().getSchemas().get("Pageable");
pageableSchema.setDescription("my description");
};
}
The same logic applies for any other attribute you nned to customize.

Making business domain objects available to Jersey Servlet Context in embedded Jetty server

Using the following dependencies (Gradle):
org.glassfish.jersey.containers:jersey-container-servlet:2.22.2
org.eclipse.jetty:jetty-servlet:9.3.2.v20150730
I have an embedded Jetty server, with a Jersey servlet container... something like this ...
package mypkg.rest.jersey;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.glassfish.jersey.server.ServerProperties;
import org.glassfish.jersey.servlet.ServletContainer;
import se.transmode.tnm.alarm.api.AlarmRetrieval;
import mypkg.rest.RestServer;
import mypkg.rest.jersey.serviceImpl.ModelAdapter;
public class JerseyBasedRestServer implements RestServer {
public static final int INITIALIZE_ON_USE = 0;
private Server server;
private final ServletContextHandler context;
private final ServletHolder servlet;
private final ModelAdapter modelAdapter;
public JerseyBasedRestServer(BusinessObjects businessObjects) {
this.modelAdapter = new ModelAdapter(businessObjects); //I want this instance to somehow be available for my ServletContainer to use.
context = new ServletContextHandler(ServletContextHandler.SESSIONS);
servlet = context.addServlet(ServletContainer.class, "/*");
servlet.setInitOrder(INITIALIZE_ON_USE);
servlet.setInitParameter(ServerProperties.PROVIDER_PACKAGES, "mypackage.jersey.generated.api.service");
servlet.setInitParameter(ServerProperties.MEDIA_TYPE_MAPPINGS, "json : application/json");
context.setContextPath("/");
}
private void startServlet() {
try {
servlet.start();
servlet.initialize();
} catch (Exception e) {
log.error("Failed to initialize servlet. {}", e.getMessage());
}
}
#Override
public void init(int port) {
server = new Server(port);
server.setHandler(context);
try {
server.start();
server.join();
startServlet();
} catch (Exception e) {
log.error("Failed to start jetty server for rest interface");
} finally {
server.destroy();
}
}
The Jersey Container will run server code and model generated using the Swagger code-gen tool
https://github.com/swagger-api/swagger-codegen#getting-started
which delivers the generated model, JacksonJsonProvider, and a RestApi class:
package mypackage.jersey.generated.api.service
Path("/")
public class RestApi {
private final RestApiService delegate = new RestApiServiceImpl(); //Integration point of the generated code
#GET
#Path("/list/")
#Consumes({ "application/json" })
#Produces({ "application/json" })
public Response retrieveAlarmList(#Context SecurityContext securityContext) throws NotFoundException {
return delegate.retrieveAlarmList(securityContext);
}
}
To integrate the generated code we are left to implement RestApiServiceImpl ourselves.
The ModelAdapter's job is to convert our business objects to the generated rest model.
So the question is how do I make the instance of the adapter of our business objects, in this case ModelAdapter, which lies outside the context of the Jersey servlet context, available to the RestApi class, or rather the RestApiServiceImpl?
I kind of understood from reading the past 24 hours that I need to use some sort of Context Dependency Injection either through Jetty, Jersey, or some other library (Weld seems to appear a lot), and have tried various combinations of #Inject, #Context, etc etc, but have come to the conclusion that I have no clue what I am actually doing... I'm not even sure I understand enough about the situation to phrase my question correctly.
More info can be made available on request.
Any help is appreciated.
EDIT: added a link here to https://github.com/englishbobster/JersetAndJetty
using #peeskillets suggestions, but still not working.
First thing you need to make DI work, is an AbstractBinder. This is where you will make your objects available to be injected.
class Binder extends AbstractBinder {
#Override
protected void configure() {
bind(modelAdapter).to(ModelAdapter.class);
}
}
Then you need to register the binder with Jersey. The easiest way is to register in Jersey's ResourceConfig. In your case, you are not using one. You are configuring everything in the "web.xml". For that, you should take a look at this post.
If you want to change your configuration to use a ResourceConfig, which personally I'd rather use, you can do this
package com.some.pkg;
public class JerseyConfig extends ResourceConfig {
public JerseyConfig() {
packages("mypackage.jersey.generated.api.service");
property(ServerProperties.MEDIA_TYPE_MAPPINGS, "json : application/json");
register(new Binder());
}
}
Then to configure it with Jetty, you can do
servlet.setInitParameter(ServletProperties.JAXRS_APPLICATION_CLASS,
"com.some.pkg.JerseyConfig");
Now you can get rid of those other two init-params, as you are configuring it inside the ResourceConfig.
Another way, without any init-params, is to do
ResourceConfig config = new JerseyConfig();
ServletHolder jerseyServlet = new ServletHolder(ServletContainer(config));
context.addServlet(jerseyServlet, "/*");
See full example of last code snippet, here.
Now you can just inject the ModelAdapter pretty much anywhere within Jersey
In a field
#Inject
private ModelAdapter adapter;
Or in a contructor
#Inject
public RestApi(ModelAdapter adapter) {
this.adapter = adapter;
}
Or method parameter
#GET
public Response get(#Context ModelAdapter adapter) {}

Resources