OpenAPI

Overview

Elide supports the generation of OpenAPI documentation from Elide annotated beans. Specifically, it generates a JSON or YAML document conforming to the OpenAPI specification that can be used by tools like Swagger UI (among others) to explore, understand, and compose queries against your Elide API.

Only JSON-API endpoints are documented. The GraphQL API schema can be explored directly with tools like Graphiql.

Features Supported

  • JaxRS & Spring Endpoint - Elide ships with a customizable JaxRS and Spring endpoints that can publish one or more OpenAPI documents in both JSON or YAML.
  • Path Discovery - Given a set of entities to explore, Elide will generate the minimum, cycle-free, de-duplicated set of URL paths in the OpenAPI document.
  • Filter by Primitive Attributes - All GET requests on entity collections include filter parameters for each primitive attribute.
  • Prune Fields - All GET requests support JSON-API sparse fields query parameter.
  • Include Top Level Relationships - All GET requests support the ability to include direct relationships.
  • Sort by Attribute - All GET requests support sort query parameters.
  • Pagination - All GET requests support pagination query parameters.
  • Permission Exposition - Elide permissions are exported as documentation for entity schemas.
  • Model & Attribute Properties - The required, readOnly, example, value, description, title, accessMode, requiredMode fields are extracted from Schema annotations.
  • API Version Support - Elide can create separate documents for different API versions.

Getting Started

Maven

If you are not using Elide Spring Starter or Elide Standalone (which package swagger as a dependency), you will need to pull in the following elide dependencies :

<dependency>
  <groupId>com.yahoo.elide</groupId>
  <artifactId>elide-swagger</artifactId>
</dependency>

<dependency>
  <groupId>com.yahoo.elide</groupId>
  <artifactId>elide-core</artifactId>
</dependency>

Pull in swagger core :

<dependency>
  <groupId>io.swagger</groupId>
  <artifactId>swagger-core-jakarta</artifactId>
</dependency>

Spring Boot Configuration

If you are using Elide Spring Autoconfigure, you can customize the OpenAPI document by using a OpenApiDocumentCustomizer bean:

@Configuration
public class ElideConfiguration {
    @Bean
    public OpenApiDocumentCustomizer openApiDocumentCustomizer() {
        return openApi -> {
            Info info = new Info()
                .title("My Title");
            openApi.setInfo(info);
        };
    }
}    

The application yaml file has settings:

  • to enable the OpenAPI document endpoint
  • to set the URL path for the OpenAPI document endpoint
  • to set the OpenAPI specification version to generate either 3.0 or 3.1
elide:
  api-docs:
    enabled: true
    path: /doc
    version: openapi_3_0

Supporting OAuth

If you want Swagger UI to acquire & use a bearer token from an OAuth identity provider, you can configure the OpenAPI document by using annotations:

@SpringBootApplication
@OpenAPIDefinition(info = @Info(title = "My Title"), security = @SecurityRequirement(name = "bearerAuth"))
@SecurityScheme(
        name = "bearerAuth",
        type = SecuritySchemeType.HTTP,
        bearerFormat = "JWT",
        scheme = "bearer"
    )
public class App {
    public static void main(String[] args) throws Exception {
        SpringApplication.run(App.class, args);
    }
}

SpringDoc Integration

Elide contributes to Springdoc’s OpenAPI document by exposing a Springdoc OpenApiCustomizer bean.

If API Versioning is used, only the Path strategy is supported when integrating with Springdoc as the other strategies are difficult to document with OpenAPI.

The default implementation is implemented in DefaultElideOpenApiCustomizer. To override the behavior a ElideOpenApiCustomizer bean can be created which will cause the DefaultElideOpenApiCustomizer not to be configured.

@Configuration
public class ElideConfiguration {
    @Bean
    public ElideOpenApiCustomizer elideOpenApiCustomizer() {
        return new MyCustomElideOpenApiCustomizer();
    }
}

When GroupedOpenApi is used, the ElideOpenApiCustomizer is not applied to the groups. Instead Elide has a DefaultElideGroupedOpenApiCustomizer that will customize the GroupedOpenApi to set the appropriate OpenApiCustomizers on the GroupedOpenApi that matches the paths to match and exclude. To override the behavior a ElideGroupedOpenApiCustomizer can be defined that will need to process the OpenApiCustomizers and remove the ones automatically added by Elide.

@Configuration
public class ElideConfiguration {
    @Bean
    public ElideGroupedOpenApiCustomizer elideGroupedOpenApiCustomizer() {
        return new MyCustomElideGroupedOpenApiCustomizer();
    }
}

Elide Standalone Configuration

If you are using Elide Standalone, you can extend ElideStandaloneSettings to:

  • Enable the OpenAPI document endpoint.
  • Configure the URL Path for the OpenAPI document.
  • Configure the OpenAPI document version.
  • Configure the service name.
  • Construct OpenAPI documents for your service.
public abstract class Settings implements ElideStandaloneSettings {
    /**
     * Enable OpenAPI documentation.
     * @return whether OpenAPI is enabled;
     */
    @Override
    public boolean enableApiDocs() {
        return true;
    }

    /**
     * API root path specification for the OpenAPI endpoint. Namely, this is the root uri for OpenAPI docs.
     *
     * @return Default: /api-docs/*
     */
    @Override
    public String getApiDocsPathSpec() {
        return "/api-docs/*";
    }

    /**
     * OpenAPI documentation requires an API name.
     * @return open api service name;
     */
    @Override
    public String getApiTitle() {
        return "Elide Service";
    }

    /**
     * The OpenAPI Specification Version to generate.
     * @return the OpenAPI Specification Version to generate
     */
    @Override
    public OpenApiVersion getOpenApiVersion() {
        return OpenApiVersion.OPENAPI_3_0;
    }

    /**
     * Creates a singular OpenAPI document for JSON-API.
     * @param dictionary Contains the static metadata about Elide models. .
     * @return list of OpenAPI registration objects.
     */
    @Override
    public List<ApiDocsEndpoint.ApiDocsRegistration> buildApiDocs(EntityDictionary dictionary) {
        List<ApiDocsEndpoint.ApiDocsRegistration> docs = new ArrayList<>();

        dictionary.getApiVersions().stream().forEach(apiVersion -> {
            Info info = new Info()
                    .title(getApiTitle())
                    .version(apiVersion);
            OpenApiBuilder builder = new OpenApiBuilder(dictionary).apiVersion(apiVersion);
            String moduleBasePath = getJsonApiPathSpec().replace("/*", "");
            OpenAPI openApi = builder.build().info(info).addServersItem(new Server().url(moduleBasePath));
            docs.add(new ApiDocsEndpoint.ApiDocsRegistration("test", () -> openApi, getOpenApiVersion().getValue(),
                    apiVersion));
        });

        return docs;
    }
}

Elide Library Configuration

If you are using Elide directly as a library (and not using Elide Standalone), follow these instructions:

Create and initialize an entity dictionary.

EntityDictionary dictionary = EntityDictionary.builder().build();

dictionary.bindEntity(Book.class);
dictionary.bindEntity(Author.class);
dictionary.bindEntity(Publisher.class);

Create a Info object.

Info info = new Info().title("My Service").version("1");

Initialize a OpenAPI builder.

OpenApiBuilder builder = new OpenApiBuilder(dictionary);

Build the OpenAPI document

OpenAPI document = builder.build().info(info);

Convert OpenAPI to JSON or YAML

You can directly convert to JSON:

OpenApiDocument openApiDocument = new OpenApiDocument(document, OpenApiDocument.Version.from("3.0"));
String jsonOutput = openApiDocument.of(OpenApiDocument.MediaType.APPLICATION_JSON);

You can directly convert to YAML:

OpenApiDocument openApiDocument = new OpenApiDocument(document, OpenApiDocument.Version.from("3.0"));
String jsonOutput = openApiDocument.of(OpenApiDocument.MediaType.APPLICATION_YAML);

Configure JAX-RS Endpoint

Or you can use the OpenAPI document directly to configure the provided JAX-RS Endpoint:

List<ApiDocsEndpoint.ApiDocsRegistration> apiDocs = new ArrayList<>();
OpenAPI openApi = new OpenAPI();
apiDocs.add(new ApiDocsEndpoint.ApiDocsRegistration("test", () -> openApi, "3.0", ""));

//Dependency Inject the ApiDocsEndpoint JAX-RS resource
bind(apiDocs).named("apiDocs").to(new TypeLiteral<List<ApiDocsEndpoint.ApiDocsRegistration>>() {
    });

Supporting OAuth

If you want Swagger UI to acquire & use a bearer token from an OAuth identity provider, you can configure the OpenAPI document similar to:

SecurityScheme oauthDef = new SecurityScheme()
    .name("bearerAuth")
    .type(SecurityScheme.Type.HTTP)
    .bearerFormat("JWT")
    .scheme("bearer");
SecurityRequirement oauthReq = new SecurityRequirement().addList("bearerAuth");

OpenApiBuilder builder = new OpenApiBuilder(entityDictionary);
OpenAPI document = builder.build();
document.addSecurityItem(oauthReq);
document.getComponents().addSecuritySchemes("bearerAuth", oauthDef);

Adding a global parameter

A query or header parameter can be added globally to all Elide API endpoints:

Parameter oauthParam = new Parameter()
    .in("Header")
    .name("Authorization")
    .schema(new StringSchema())
    .description("OAuth bearer token")
    .required(false);

OpenApiBuilder builder = new OpenApiBuilder(dictionary)
    .globalParameter(oauthParam);

Adding a global response code

An HTTP response can be added globally to all Elide API endpoints:

ApiResponse rateLimitedResponse = new ApiResponse().description("Too Many Requests");

OpenApiBuilder builder = new OpenApiBuilder(dictionary)
    .globalResponse(429, rateLimitedResponse);

Performance

Path Generation

The Swagger UI is very slow when the number of generated URL paths exceeds a few dozen. For large, complex data models, it is recommended to generate separate OpenAPI documents for subgraphs of the model.

Set<Type<?>> entities = Set.of(
    ClassType.of(Book.class),
    ClassType.of(Author.class),
    ClassType.of(Publisher.class)
);

OpenApiBuilder builder = new OpenApiBuilder(dictionary)
    .managedClasses(entities);

In the above example, the OpenApiBuilder will only generate paths that exclusively traverse the provided set of entities.

Document Size

The size of the OpenAPI document can be reduced significantly by limiting the number of filter operators that are used to generate query parameter documentation.

OpenApiBuilder builder = new OpenApiBuilder(dictionary)
   .filterOperators(Set.of(Operator.IN));

In the above example, filter query parameters are only generated for the IN operator.

Model Properties

Elide extracts the model description and title from the Schema and Include annotations and adds them to the OpenAPI documentation. Schema has precedence over Include if both are present.

@Schema(description = "A book model description", title = "Book")
class Book {

}

For Schema only the description and title property is extracted. For the Include annotation, the friendlyName is used as the title.

Attribute Properties

Elide extracts properties from the Schema annotation and adds them to the OpenAPI documentation.

class Book {

    @Schema(requiredMode = RequiredMode.REQUIRED)
    public String title;
}

Only the required, value, example, readOnly, writeOnly, requiredProperties, requiredMode and accessMode properties are extracted. This is currently only supported for attributes on Elide models.

API Versions

OpenAPI documents are tied to an explicit API version. When constructing a OpenAPI document, the API version must be set to match the API version of the models it will describe:

OpenApiBuilder builder = new OpenApiBuilder(dictionary).apiVersion("1");
OpenAPI openApi = builder.build();