diff --git a/code/chapter04/catalog/README-OPENAPI-4.1.md b/code/chapter04/catalog/README-OPENAPI-4.1.md new file mode 100644 index 00000000..e28236dd --- /dev/null +++ b/code/chapter04/catalog/README-OPENAPI-4.1.md @@ -0,0 +1,84 @@ +# Chapter 04 - MicroProfile OpenAPI 4.1 + +This chapter demonstrates the features of MicroProfile OpenAPI 4.1, including: + +## New Features Demonstrated + +### 1. OpenAPI v3.1 Compatibility +The generated OpenAPI documents use the `openapi: 3.1.0` version, bringing better JSON Schema support and improved nullable handling. + +### 2. Java Records Support +See `ProductRecord.java` - demonstrates how Java Records are automatically documented with proper schema generation. + +Example endpoint: `GET /api/products/record/{id}` + +### 3. Optional Fields +See `ProductWithOptional.java` - shows how Optional fields are properly marked as nullable in the OpenAPI schema. + +Example endpoint: `GET /api/products/optional/{id}` + +### 4. @Target Annotation Enhancement +See `ConditionalProduct.java` - demonstrates the `@DependentRequired` annotation with proper @Target metadata. + +### 5. JSON Schema Dialect Support +See `CustomModelReader.java` - shows how to specify the JSON Schema dialect using `jsonSchemaDialect` property. + +To enable this reader, add to `META-INF/microprofile-config.properties`: +``` +mp.openapi.model.reader=io.microprofile.tutorial.store.product.CustomModelReader +``` + +### 6. Extensible Interface Methods +See `ExtensionFilter.java` - demonstrates the new `getExtension()` and `hasExtension()` methods. + +To enable this filter, add to `META-INF/microprofile-config.properties`: +``` +mp.openapi.filter=io.microprofile.tutorial.store.product.ExtensionFilter +``` + +### 7. Async Operations with Callbacks +See the `processProductAsync()` method in `ProductResource.java` - demonstrates how to document asynchronous operations with callback URLs. + +Example endpoint: `POST /api/products/async-process` + +### 8. Security Schemes +See `SecuredProductApplication.java` - comprehensive example of documenting multiple security schemes including: +- API Key authentication +- Bearer Token (JWT) +- OAuth2 with authorization code flow +- Basic HTTP authentication + +## Building and Running + +This project is configured to use Java 21 as specified in the pom.xml. However, the MicroProfile OpenAPI 4.1 features demonstrated here work with Java 17+ (which includes support for Java Records). + +If you have Java 21 installed: + +```bash +java -version # Should show version 21 +mvn clean package +mvn liberty:dev +``` + +If you only have Java 17, you can modify the pom.xml to use Java 17 instead (Java Records are supported since Java 16 and finalized in Java 17). + +## Viewing the OpenAPI Documentation + +Once the application is running, you can view the OpenAPI document at: + +- YAML format: http://localhost:5050/openapi +- Swagger UI: http://localhost:5050/openapi/ui + +## Key OpenAPI 4.1 Improvements + +1. **Better nullable handling**: Uses JSON Schema's type arrays instead of deprecated `nullable` keyword +2. **JSON Schema 2020-12 alignment**: More robust schema validation +3. **Enhanced extension support**: New methods for working with vendor extensions +4. **Improved documentation for async APIs**: Better callback support +5. **Comprehensive security documentation**: Multiple authentication mechanisms + +## References + +- [MicroProfile OpenAPI 4.1 Specification](https://download.eclipse.org/microprofile/microprofile-open-api-4.1/microprofile-openapi-spec-4.1.html) +- [OpenAPI v3.1 Specification](https://spec.openapis.org/oas/v3.1.0.html) +- [JSON Schema 2020-12](https://json-schema.org/draft/2020-12/json-schema-core.html) diff --git a/code/chapter04/catalog/src/main/java/io/microprofile/tutorial/store/product/CustomModelReader.java b/code/chapter04/catalog/src/main/java/io/microprofile/tutorial/store/product/CustomModelReader.java new file mode 100644 index 00000000..12c05a9a --- /dev/null +++ b/code/chapter04/catalog/src/main/java/io/microprofile/tutorial/store/product/CustomModelReader.java @@ -0,0 +1,24 @@ +package io.microprofile.tutorial.store.product; + +import org.eclipse.microprofile.openapi.OASFactory; +import org.eclipse.microprofile.openapi.OASModelReader; +import org.eclipse.microprofile.openapi.models.OpenAPI; + +/** + * Custom OpenAPI model reader demonstrating jsonSchemaDialect support. + * This reader is called once during application startup to build/enhance the OpenAPI model. + */ +public class CustomModelReader implements OASModelReader { + + @Override + public OpenAPI buildModel() { + // Create an OpenAPI object with jsonSchemaDialect + return OASFactory.createOpenAPI() + .openapi("3.1.0") + .jsonSchemaDialect("https://spec.openapis.org/oas/3.1/dialect/base") + .info(OASFactory.createInfo() + .title("Product API") + .version("1.0.0") + .description("API for managing products with MicroProfile OpenAPI 4.1")); + } +} diff --git a/code/chapter04/catalog/src/main/java/io/microprofile/tutorial/store/product/ExtensionFilter.java b/code/chapter04/catalog/src/main/java/io/microprofile/tutorial/store/product/ExtensionFilter.java new file mode 100644 index 00000000..5b18761b --- /dev/null +++ b/code/chapter04/catalog/src/main/java/io/microprofile/tutorial/store/product/ExtensionFilter.java @@ -0,0 +1,34 @@ +package io.microprofile.tutorial.store.product; + +import org.eclipse.microprofile.openapi.OASFilter; +import org.eclipse.microprofile.openapi.models.Operation; + +/** + * OpenAPI filter demonstrating the use of getExtension() and hasExtension() methods. + * This filter is called for each element in the OpenAPI model tree. + */ +public class ExtensionFilter implements OASFilter { + + @Override + public Operation filterOperation(Operation operation) { + // Check if a custom extension exists using the new hasExtension method + if (operation.hasExtension("x-custom-timeout")) { + // Retrieve the extension value using the new getExtension method + Object timeout = operation.getExtension("x-custom-timeout"); + System.out.println("Custom timeout found: " + timeout); + + // Modify based on the extension + if (timeout != null && timeout instanceof Integer && (Integer) timeout > 30) { + operation.addExtension("x-requires-approval", true); + } + } + + // Check for rate limiting extension + if (operation.hasExtension("x-rate-limit")) { + Object rateLimit = operation.getExtension("x-rate-limit"); + System.out.println("Rate limit: " + rateLimit); + } + + return operation; + } +} diff --git a/code/chapter04/catalog/src/main/java/io/microprofile/tutorial/store/product/SecuredProductApplication.java b/code/chapter04/catalog/src/main/java/io/microprofile/tutorial/store/product/SecuredProductApplication.java new file mode 100644 index 00000000..97f326d4 --- /dev/null +++ b/code/chapter04/catalog/src/main/java/io/microprofile/tutorial/store/product/SecuredProductApplication.java @@ -0,0 +1,87 @@ +package io.microprofile.tutorial.store.product; + +import org.eclipse.microprofile.openapi.annotations.OpenAPIDefinition; +import org.eclipse.microprofile.openapi.annotations.security.SecurityScheme; +import org.eclipse.microprofile.openapi.annotations.security.SecuritySchemes; +import org.eclipse.microprofile.openapi.annotations.enums.SecuritySchemeType; +import org.eclipse.microprofile.openapi.annotations.enums.SecuritySchemeIn; +import org.eclipse.microprofile.openapi.annotations.security.OAuthFlows; +import org.eclipse.microprofile.openapi.annotations.security.OAuthFlow; +import org.eclipse.microprofile.openapi.annotations.security.OAuthScope; +import org.eclipse.microprofile.openapi.annotations.info.Info; +import org.eclipse.microprofile.openapi.annotations.info.Contact; +import org.eclipse.microprofile.openapi.annotations.info.License; +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.core.Application; + +/** + * Application class demonstrating security scheme definitions in MicroProfile OpenAPI 4.1. + * This shows how to document multiple security mechanisms for your API. + */ +@ApplicationPath("/api") +@OpenAPIDefinition( + info = @Info( + title = "Secured Product API", + version = "1.0.0", + description = "Product API with multiple security schemes demonstrating MicroProfile OpenAPI 4.1 capabilities", + contact = @Contact( + name = "API Support", + email = "support@example.com", + url = "https://example.com/support" + ), + license = @License( + name = "Apache 2.0", + url = "https://www.apache.org/licenses/LICENSE-2.0.html" + ) + ) +) +@SecuritySchemes({ + @SecurityScheme( + securitySchemeName = "apiKey", + type = SecuritySchemeType.APIKEY, + description = "API Key authentication - provide your API key in the X-API-Key header", + in = SecuritySchemeIn.HEADER, + apiKeyName = "X-API-Key" + ), + @SecurityScheme( + securitySchemeName = "bearerAuth", + type = SecuritySchemeType.HTTP, + description = "JWT Bearer token authentication - obtain token from /auth/login endpoint", + scheme = "bearer", + bearerFormat = "JWT" + ), + @SecurityScheme( + securitySchemeName = "oauth2", + type = SecuritySchemeType.OAUTH2, + description = "OAuth2 authentication with authorization code flow", + flows = @OAuthFlows( + authorizationCode = @OAuthFlow( + authorizationUrl = "https://example.com/oauth/authorize", + tokenUrl = "https://example.com/oauth/token", + refreshUrl = "https://example.com/oauth/refresh", + scopes = { + @OAuthScope( + name = "read:products", + description = "Read product information" + ), + @OAuthScope( + name = "write:products", + description = "Create and modify product information" + ), + @OAuthScope( + name = "delete:products", + description = "Delete product information" + ) + } + ) + ) + ), + @SecurityScheme( + securitySchemeName = "basicAuth", + type = SecuritySchemeType.HTTP, + description = "Basic HTTP authentication", + scheme = "basic" + ) +}) +public class SecuredProductApplication extends Application { +} diff --git a/code/chapter04/catalog/src/main/java/io/microprofile/tutorial/store/product/entity/AsyncRequest.java b/code/chapter04/catalog/src/main/java/io/microprofile/tutorial/store/product/entity/AsyncRequest.java new file mode 100644 index 00000000..95e86171 --- /dev/null +++ b/code/chapter04/catalog/src/main/java/io/microprofile/tutorial/store/product/entity/AsyncRequest.java @@ -0,0 +1,42 @@ +package io.microprofile.tutorial.store.product.entity; + +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +/** + * Request object for async product processing. + */ +@Schema(description = "Async product processing request") +public class AsyncRequest { + + @Schema(description = "Product to process", required = true) + private Product product; + + @Schema(description = "Callback URL to notify when processing completes", + example = "https://example.com/callback", + required = true) + private String callbackUrl; + + public AsyncRequest() { + } + + public AsyncRequest(Product product, String callbackUrl) { + this.product = product; + this.callbackUrl = callbackUrl; + } + + public Product getProduct() { + return product; + } + + public void setProduct(Product product) { + this.product = product; + } + + public String getCallbackUrl() { + return callbackUrl; + } + + public void setCallbackUrl(String callbackUrl) { + this.callbackUrl = callbackUrl; + } +} diff --git a/code/chapter04/catalog/src/main/java/io/microprofile/tutorial/store/product/entity/ConditionalProduct.java b/code/chapter04/catalog/src/main/java/io/microprofile/tutorial/store/product/entity/ConditionalProduct.java new file mode 100644 index 00000000..8ca1b1e7 --- /dev/null +++ b/code/chapter04/catalog/src/main/java/io/microprofile/tutorial/store/product/entity/ConditionalProduct.java @@ -0,0 +1,85 @@ +package io.microprofile.tutorial.store.product.entity; + +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.media.DependentRequired; + +/** + * Product with conditional validation. + * Demonstrates the @DependentRequired annotation in MicroProfile OpenAPI 4.1. + */ +@Schema( + description = "Product with conditional validation", + dependentRequired = { + @DependentRequired( + name = "discount", + requires = {"discountReason"} + ) + } +) +public class ConditionalProduct { + + @Schema(description = "Product ID", example = "1") + private Long id; + + @Schema(description = "Product name", example = "Laptop", required = true) + private String name; + + @Schema(description = "Product price", example = "999.99", required = true) + private Double price; + + @Schema(description = "Discount amount (requires discountReason if set)", example = "100.0") + private Double discount; + + @Schema(description = "Reason for discount", example = "Black Friday Sale") + private String discountReason; + + public ConditionalProduct() { + } + + public ConditionalProduct(Long id, String name, Double price) { + this.id = id; + this.name = name; + this.price = price; + } + + // Getters and setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Double getPrice() { + return price; + } + + public void setPrice(Double price) { + this.price = price; + } + + public Double getDiscount() { + return discount; + } + + public void setDiscount(Double discount) { + this.discount = discount; + } + + public String getDiscountReason() { + return discountReason; + } + + public void setDiscountReason(String discountReason) { + this.discountReason = discountReason; + } +} diff --git a/code/chapter04/catalog/src/main/java/io/microprofile/tutorial/store/product/entity/ProcessResult.java b/code/chapter04/catalog/src/main/java/io/microprofile/tutorial/store/product/entity/ProcessResult.java new file mode 100644 index 00000000..1d9fedb6 --- /dev/null +++ b/code/chapter04/catalog/src/main/java/io/microprofile/tutorial/store/product/entity/ProcessResult.java @@ -0,0 +1,68 @@ +package io.microprofile.tutorial.store.product.entity; + +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +/** + * Result object for async product processing. + */ +@Schema(description = "Result of async product processing") +public class ProcessResult { + + @Schema(description = "ID of the processed product", example = "123") + private Long productId; + + @Schema(description = "Processing status", + example = "COMPLETED", + enumeration = {"COMPLETED", "FAILED", "PENDING"}) + private String status; + + @Schema(description = "Processing message or error details", + example = "Product processed successfully") + private String message; + + @Schema(description = "Timestamp when processing completed", + example = "2025-01-31T16:00:00Z") + private String timestamp; + + public ProcessResult() { + } + + public ProcessResult(Long productId, String status, String message) { + this.productId = productId; + this.status = status; + this.message = message; + this.timestamp = java.time.Instant.now().toString(); + } + + public Long getProductId() { + return productId; + } + + public void setProductId(Long productId) { + this.productId = productId; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getTimestamp() { + return timestamp; + } + + public void setTimestamp(String timestamp) { + this.timestamp = timestamp; + } +} diff --git a/code/chapter04/catalog/src/main/java/io/microprofile/tutorial/store/product/entity/ProductRecord.java b/code/chapter04/catalog/src/main/java/io/microprofile/tutorial/store/product/entity/ProductRecord.java new file mode 100644 index 00000000..c9ad6c66 --- /dev/null +++ b/code/chapter04/catalog/src/main/java/io/microprofile/tutorial/store/product/entity/ProductRecord.java @@ -0,0 +1,37 @@ +package io.microprofile.tutorial.store.product.entity; + +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +/** + * Product information as a Java Record. + * Demonstrates MicroProfile OpenAPI 4.1 support for Java Records. + */ +@Schema(name = "ProductRecord", description = "Product information as a Java Record") +public record ProductRecord( + @Schema(description = "Product ID", example = "1") + Long id, + + @Schema(description = "Product name", example = "Laptop", required = true) + String name, + + @Schema(description = "Product description", example = "High-performance laptop") + String description, + + @Schema(description = "Product price", example = "999.99", required = true) + Double price +) { + /** + * Create a ProductRecord from a Product entity. + * + * @param product the product entity + * @return a new ProductRecord + */ + public static ProductRecord fromProduct(Product product) { + return new ProductRecord( + product.getId(), + product.getName(), + product.getDescription(), + product.getPrice() + ); + } +} diff --git a/code/chapter04/catalog/src/main/java/io/microprofile/tutorial/store/product/entity/ProductWithOptional.java b/code/chapter04/catalog/src/main/java/io/microprofile/tutorial/store/product/entity/ProductWithOptional.java new file mode 100644 index 00000000..19b6ea49 --- /dev/null +++ b/code/chapter04/catalog/src/main/java/io/microprofile/tutorial/store/product/entity/ProductWithOptional.java @@ -0,0 +1,90 @@ +package io.microprofile.tutorial.store.product.entity; + +import java.util.Optional; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +/** + * Product with optional fields. + * Demonstrates MicroProfile OpenAPI 4.1 support for Optional types. + */ +@Schema(description = "Product with optional fields") +public class ProductWithOptional { + + @Schema(description = "Product ID", required = true) + private Long id; + + @Schema(description = "Product name", required = true) + private String name; + + @Schema(description = "Optional product description") + private Optional description = Optional.empty(); + + @Schema(description = "Optional product category") + private Optional category = Optional.empty(); + + @Schema(description = "Product price", required = true) + private Double price; + + public ProductWithOptional() { + } + + public ProductWithOptional(Long id, String name, Double price) { + this.id = id; + this.name = name; + this.price = price; + } + + // Getters and setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Optional getDescription() { + return description; + } + + public void setDescription(Optional description) { + this.description = description; + } + + public Optional getCategory() { + return category; + } + + public void setCategory(Optional category) { + this.category = category; + } + + public Double getPrice() { + return price; + } + + public void setPrice(Double price) { + this.price = price; + } + + /** + * Create from a regular Product. + */ + public static ProductWithOptional fromProduct(Product product) { + ProductWithOptional p = new ProductWithOptional( + product.getId(), + product.getName(), + product.getPrice() + ); + p.setDescription(Optional.ofNullable(product.getDescription())); + return p; + } +} diff --git a/code/chapter04/catalog/src/main/java/io/microprofile/tutorial/store/product/resource/ProductResource.java b/code/chapter04/catalog/src/main/java/io/microprofile/tutorial/store/product/resource/ProductResource.java index b10f589b..c1896337 100644 --- a/code/chapter04/catalog/src/main/java/io/microprofile/tutorial/store/product/resource/ProductResource.java +++ b/code/chapter04/catalog/src/main/java/io/microprofile/tutorial/store/product/resource/ProductResource.java @@ -5,8 +5,12 @@ import java.util.logging.Logger; import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.callbacks.Callback; +import org.eclipse.microprofile.openapi.annotations.callbacks.CallbackOperation; +import org.eclipse.microprofile.openapi.annotations.extensions.Extension; import org.eclipse.microprofile.openapi.annotations.media.Content; import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody; import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; import org.eclipse.microprofile.openapi.annotations.tags.Tag; @@ -159,4 +163,115 @@ public Response searchProducts( List results = productService.searchProducts(name, description, minPrice, maxPrice); return Response.ok(results).build(); } + + // ===== New MicroProfile OpenAPI 4.1 Features Examples ===== + + @GET + @Path("/record/{id}") + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Get product as Java Record", + description = "Returns product data using Java Record (MicroProfile OpenAPI 4.1 feature)", + extensions = { + @Extension(name = "x-custom-timeout", value = "60"), + @Extension(name = "x-rate-limit", value = "100") + } + ) + @APIResponse( + responseCode = "200", + description = "Product found", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = io.microprofile.tutorial.store.product.entity.ProductRecord.class) + ) + ) + @APIResponse(responseCode = "404", description = "Product not found") + public Response getProductRecord(@PathParam("id") Long id) { + LOGGER.info("REST: Fetching product record with id: " + id); + Product product = productService.findProductById(id); + if (product != null) { + var record = io.microprofile.tutorial.store.product.entity.ProductRecord.fromProduct(product); + return Response.ok(record).build(); + } else { + return Response.status(Response.Status.NOT_FOUND).build(); + } + } + + @GET + @Path("/optional/{id}") + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Get product with Optional fields", + description = "Returns product with Optional fields (MicroProfile OpenAPI 4.1 feature)" + ) + @APIResponse( + responseCode = "200", + description = "Product found", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = io.microprofile.tutorial.store.product.entity.ProductWithOptional.class) + ) + ) + public Response getProductWithOptional(@PathParam("id") Long id) { + LOGGER.info("REST: Fetching product with optional fields, id: " + id); + Product product = productService.findProductById(id); + if (product != null) { + var productOptional = io.microprofile.tutorial.store.product.entity.ProductWithOptional.fromProduct(product); + return Response.ok(productOptional).build(); + } else { + return Response.status(Response.Status.NOT_FOUND).build(); + } + } + + @POST + @Path("/async-process") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Process product asynchronously", + description = "Initiates async product processing and calls back when complete (MicroProfile OpenAPI 4.1 async feature)" + ) + @Callback( + name = "productProcessed", + callbackUrlExpression = "{$request.body#/callbackUrl}", + operations = { + @CallbackOperation( + method = "post", + summary = "Product processing completed", + requestBody = @RequestBody( + description = "Processing result", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = io.microprofile.tutorial.store.product.entity.ProcessResult.class) + ) + ) + ) + } + ) + @APIResponse( + responseCode = "202", + description = "Processing initiated" + ) + @APIResponse( + responseCode = "400", + description = "Invalid request" + ) + public Response processProductAsync( + @RequestBody( + description = "Product and callback URL", + required = true, + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = io.microprofile.tutorial.store.product.entity.AsyncRequest.class) + ) + ) io.microprofile.tutorial.store.product.entity.AsyncRequest request + ) { + LOGGER.info("REST: Initiating async product processing"); + // In a real application, this would trigger async processing + // For demo purposes, we'll just return accepted + return Response.accepted() + .entity("{\"message\": \"Processing initiated\", \"requestId\": \"" + + java.util.UUID.randomUUID() + "\"}") + .build(); + } } \ No newline at end of file diff --git a/modules/ROOT/pages/chapter04/chapter04.adoc b/modules/ROOT/pages/chapter04/chapter04.adoc index a25f56a0..5f0f12b9 100644 --- a/modules/ROOT/pages/chapter04/chapter04.adoc +++ b/modules/ROOT/pages/chapter04/chapter04.adoc @@ -14,6 +14,15 @@ This chapter will explore the primary features of MicroProfile OpenAPI, demonstr - Generating API Documentation - Documenting Authentication and Authorization Requirements - Exploring the APIs using Swagger UI +- New Features in MicroProfile OpenAPI 4.1: + * OpenAPI v3.1 compatibility + * Java Records support + * @Target annotation enhancements + * JSON Schema Dialect support + * Extensible interface methods + * Async operations with callbacks + * Optional nullable fields + * Comprehensive security schemes == OpenAPI Specification @@ -31,7 +40,7 @@ The specification aims to provide a uniform way of describing APIs so that they == Capabilities of MicroProfile OpenAPI Specification -MicroProfile OpenAPI provides a suite of Java APIs that allows developers to define and generate API specifications that adhere to OpenAPI v3 standards. As a result, it simplifies the process of designing, documenting, and publishing RESTful APIs for developers. +MicroProfile OpenAPI provides a suite of Java APIs that allows developers to define and generate API specifications that adhere to OpenAPI v3.1 standards. As a result, it simplifies the process of designing, documenting, and publishing RESTful APIs for developers. Developers can quickly generate documentation for their microservices using MicroProfile OpenAPI. The documentation includes information on what services are provided, how to invoke them, and what data types are used. It generates comprehensive metadata about services, ensuring interoperability across diverse platforms and tools. Also, documentation can generate client code to access the web services. @@ -59,7 +68,7 @@ To use MicroProfile OpenAPI in your project, you need to add the following maven org.eclipse.microprofile.openapi microprofile-openapi-api - 3.1.1 + 4.1 ---- @@ -147,7 +156,7 @@ When we access the `http://localhost:5050/openapi` URL, we should see the API do [source, yaml] ---- -openapi: 3.0.3 +openapi: 3.1.0 info: title: Generated API version: "1.0" @@ -318,6 +327,576 @@ The MicroProfile OpenAPI annotations can be used to document any Jakarta Restful All of these annotations are defined in the org.eclipse.microprofile.openapi.annotations package. +== New Features in MicroProfile OpenAPI 4.1 + +MicroProfile OpenAPI 4.1 introduces several significant enhancements that align with the OpenAPI v3.1 specification and improve developer productivity. This section explores the key features introduced in version 4.1. + +=== Compatibility with OpenAPI v3.1 + +MicroProfile OpenAPI 4.1 is fully compatible with the OpenAPI v3.1 specification, which brings several improvements over the previous version: + +* *JSON Schema 2020-12 Support*: OpenAPI v3.1 aligns with JSON Schema 2020-12, providing more robust schema validation and better interoperability with JSON Schema tools. + +* *Improved nullable handling*: In OpenAPI v3.1, nullable types are represented using JSON Schema's type arrays (e.g., `type: ["string", "null"]`) instead of the deprecated `nullable` keyword. + +* *Enhanced Schema composition*: Better support for `oneOf`, `anyOf`, and `allOf` for more flexible schema modeling. + +Example of OpenAPI v3.1 output: + +[source, yaml] +---- +openapi: 3.1.0 +info: + title: Product API + version: "1.0" +paths: + /api/products: + get: + responses: + "200": + description: Successful response + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Product' +---- + +=== Support for Java Records + +Java Records, introduced in Java 14 and finalized in Java 16, provide a concise way to declare immutable data carriers. MicroProfile OpenAPI 4.1 provides enhanced support for Java Records, automatically generating appropriate schema definitions for record types. + +Example: Using Java Records with MicroProfile OpenAPI + +[source, java] +---- +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Schema(name = "ProductRecord", description = "Product information as a Java Record") +public record ProductRecord( + @Schema(description = "Product ID", example = "1") + Long id, + + @Schema(description = "Product name", example = "Laptop", required = true) + String name, + + @Schema(description = "Product description", example = "High-performance laptop") + String description, + + @Schema(description = "Product price", example = "999.99", required = true) + Double price +) {} +---- + +Using the record in a REST resource: + +[source, java] +---- +@GET +@Path("/record/{id}") +@Produces(MediaType.APPLICATION_JSON) +@Operation(summary = "Get product as record", description = "Returns product data using Java Record") +@APIResponse( + responseCode = "200", + description = "Product found", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ProductRecord.class) + ) +) +public ProductRecord getProductRecord(@PathParam("id") Long id) { + Product product = productService.findProductById(id); + return new ProductRecord(product.getId(), product.getName(), + product.getDescription(), product.getPrice()); +} +---- + +The generated OpenAPI schema for the record: + +[source, yaml] +---- +components: + schemas: + ProductRecord: + description: Product information as a Java Record + required: + - name + - price + type: object + properties: + id: + description: Product ID + type: integer + format: int64 + example: 1 + name: + description: Product name + type: string + example: Laptop + description: + description: Product description + type: string + example: High-performance laptop + price: + description: Product price + type: number + format: double + example: 999.99 +---- + +=== @Target Annotation Enhancement + +MicroProfile OpenAPI 4.1 adds proper `@Target` annotations to several schema-related annotations, improving type safety and IDE support. The annotations `@DependentRequired`, `@DependentSchema`, and `@SchemaProperty` now have explicit target definitions. + +Example: Using @DependentRequired and @DependentSchema + +[source, java] +---- +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.media.DependentRequired; +import org.eclipse.microprofile.openapi.annotations.media.DependentSchema; +import org.eclipse.microprofile.openapi.annotations.media.SchemaProperty; + +@Schema( + description = "Product with conditional validation", + dependentRequired = { + @DependentRequired( + name = "discount", + requires = {"discountReason"} + ) + } +) +public class ConditionalProduct { + private Long id; + private String name; + private Double price; + private Double discount; + private String discountReason; + + // Getters and setters +} +---- + +This ensures that if a `discount` field is provided, the `discountReason` field must also be included in the request. + +=== JSON Schema Dialect Support + +MicroProfile OpenAPI 4.1 introduces the `jsonSchemaDialect` property, which allows you to specify which JSON Schema dialect is used in your OpenAPI document. This is particularly useful when working with tools that require specific JSON Schema versions. + +Example: Using jsonSchemaDialect programmatically + +[source, java] +---- +import org.eclipse.microprofile.openapi.OASFactory; +import org.eclipse.microprofile.openapi.OASModelReader; +import org.eclipse.microprofile.openapi.models.OpenAPI; + +public class CustomModelReader implements OASModelReader { + + @Override + public OpenAPI buildModel() { + return OASFactory.createOpenAPI() + .openapi("3.1.0") + .jsonSchemaDialect("https://spec.openapis.org/oas/3.1/dialect/base") + .info(OASFactory.createInfo() + .title("Product API") + .version("1.0.0") + .description("API for managing products")); + } +} +---- + +The resulting OpenAPI document will include: + +[source, yaml] +---- +openapi: 3.1.0 +jsonSchemaDialect: https://spec.openapis.org/oas/3.1/dialect/base +info: + title: Product API + version: 1.0.0 + description: API for managing products +---- + +=== Extensible Interface Methods + +MicroProfile OpenAPI 4.1 adds two new methods to the `Extensible` interface: `getExtension(String)` and `hasExtension(String)`. These methods provide a more convenient way to work with vendor extensions in your OpenAPI model. + +Example: Using the new Extensible interface methods + +[source, java] +---- +import org.eclipse.microprofile.openapi.OASFactory; +import org.eclipse.microprofile.openapi.OASFilter; +import org.eclipse.microprofile.openapi.models.OpenAPI; +import org.eclipse.microprofile.openapi.models.Operation; + +public class ExtensionFilter implements OASFilter { + + @Override + public Operation filterOperation(Operation operation) { + // Check if a custom extension exists + if (operation.hasExtension("x-custom-timeout")) { + Object timeout = operation.getExtension("x-custom-timeout"); + System.out.println("Custom timeout found: " + timeout); + + // Modify based on the extension + if (timeout != null && (Integer) timeout > 30) { + operation.addExtension("x-requires-approval", true); + } + } + return operation; + } +} +---- + +Adding extensions to an operation: + +[source, java] +---- +@GET +@Path("/{id}") +@Produces(MediaType.APPLICATION_JSON) +@Operation( + summary = "Get product by ID", + extensions = { + @Extension(name = "x-custom-timeout", value = "60"), + @Extension(name = "x-rate-limit", value = "100") + } +) +public Response getProductById(@PathParam("id") Long id) { + // Method implementation +} +---- + +=== Using @Operation for Async Methods + +MicroProfile OpenAPI 4.1 supports documenting asynchronous operations using the `@Operation` annotation in combination with callbacks. This is particularly useful for webhook-style APIs or operations that trigger asynchronous processing. + +Example: Documenting an async operation with callbacks + +[source, java] +---- +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.callbacks.Callback; +import org.eclipse.microprofile.openapi.annotations.callbacks.CallbackOperation; +import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; + +@POST +@Path("/async-process") +@Operation( + summary = "Process product asynchronously", + description = "Initiates async product processing and calls back when complete" +) +@Callback( + name = "productProcessed", + callbackUrlExpression = "{$request.body#/callbackUrl}", + operations = { + @CallbackOperation( + method = "post", + summary = "Product processing completed", + requestBody = @RequestBody( + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ProcessResult.class) + ) + ) + ) + } +) +public Response processProductAsync( + @RequestBody( + description = "Product and callback URL", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = AsyncRequest.class) + ) + ) AsyncRequest request +) { + // Initiate async processing + return Response.accepted().build(); +} + +// Supporting classes +public static class AsyncRequest { + private Product product; + private String callbackUrl; + // Getters and setters +} + +public static class ProcessResult { + private Long productId; + private String status; + private String message; + // Getters and setters +} +---- + +=== Using Optional Fields as Nullable + +MicroProfile OpenAPI 4.1, with its alignment to OpenAPI v3.1, provides better handling of Java's `Optional` type. Fields declared as `Optional` are automatically treated as nullable in the generated schema. + +Example: Using Optional fields + +[source, java] +---- +import java.util.Optional; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Schema(description = "Product with optional fields") +public class ProductWithOptional { + + @Schema(description = "Product ID", required = true) + private Long id; + + @Schema(description = "Product name", required = true) + private String name; + + @Schema(description = "Optional product description") + private Optional description; + + @Schema(description = "Optional product category") + private Optional category; + + @Schema(description = "Product price", required = true) + private Double price; + + // Getters and setters + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public Optional getDescription() { return description; } + public void setDescription(Optional description) { + this.description = description; + } + + public Optional getCategory() { return category; } + public void setCategory(Optional category) { + this.category = category; + } + + public Double getPrice() { return price; } + public void setPrice(Double price) { this.price = price; } +} +---- + +The generated schema correctly identifies optional fields as nullable: + +[source, yaml] +---- +components: + schemas: + ProductWithOptional: + description: Product with optional fields + required: + - id + - name + - price + type: object + properties: + id: + description: Product ID + type: integer + format: int64 + name: + description: Product name + type: string + description: + description: Optional product description + type: + - string + - null + category: + description: Optional product category + type: + - string + - null + price: + description: Product price + type: number + format: double +---- + +=== Using Security Schemes in OpenAPI Documentation + +Security is a critical aspect of API documentation. MicroProfile OpenAPI 4.1 provides comprehensive support for documenting various security schemes including API keys, HTTP authentication, OAuth2, and OpenID Connect. + +Example: Defining security schemes + +[source, java] +---- +import org.eclipse.microprofile.openapi.annotations.OpenAPIDefinition; +import org.eclipse.microprofile.openapi.annotations.security.SecurityScheme; +import org.eclipse.microprofile.openapi.annotations.security.SecuritySchemes; +import org.eclipse.microprofile.openapi.annotations.enums.SecuritySchemeType; +import org.eclipse.microprofile.openapi.annotations.enums.SecuritySchemeIn; +import org.eclipse.microprofile.openapi.annotations.security.OAuthFlows; +import org.eclipse.microprofile.openapi.annotations.security.OAuthFlow; +import org.eclipse.microprofile.openapi.annotations.security.OAuthScope; +import org.eclipse.microprofile.openapi.annotations.info.Info; +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.core.Application; + +@ApplicationPath("/api") +@OpenAPIDefinition( + info = @Info( + title = "Secured Product API", + version = "1.0.0", + description = "Product API with multiple security schemes" + ) +) +@SecuritySchemes({ + @SecurityScheme( + securitySchemeName = "apiKey", + type = SecuritySchemeType.APIKEY, + description = "API Key authentication", + in = SecuritySchemeIn.HEADER, + apiKeyName = "X-API-Key" + ), + @SecurityScheme( + securitySchemeName = "bearerAuth", + type = SecuritySchemeType.HTTP, + description = "JWT Bearer token authentication", + scheme = "bearer", + bearerFormat = "JWT" + ), + @SecurityScheme( + securitySchemeName = "oauth2", + type = SecuritySchemeType.OAUTH2, + description = "OAuth2 authentication", + flows = @OAuthFlows( + authorizationCode = @OAuthFlow( + authorizationUrl = "https://example.com/oauth/authorize", + tokenUrl = "https://example.com/oauth/token", + scopes = { + @OAuthScope(name = "read:products", description = "Read product information"), + @OAuthScope(name = "write:products", description = "Modify product information") + } + ) + ) + ) +}) +public class SecuredProductApplication extends Application { +} +---- + +Applying security to specific operations: + +[source, java] +---- +import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement; +import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirements; + +@GET +@Path("/{id}") +@Produces(MediaType.APPLICATION_JSON) +@Operation(summary = "Get product by ID", description = "Requires authentication") +@SecurityRequirement(name = "bearerAuth") +@APIResponse( + responseCode = "200", + description = "Product found", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Product.class) + ) +) +@APIResponse(responseCode = "401", description = "Unauthorized") +@APIResponse(responseCode = "404", description = "Product not found") +public Response getSecuredProduct(@PathParam("id") Long id) { + // Method implementation +} + +@POST +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@Operation(summary = "Create a new product", description = "Requires OAuth2 write scope") +@SecurityRequirement(name = "oauth2", scopes = {"write:products"}) +@APIResponse( + responseCode = "201", + description = "Product created", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Product.class) + ) +) +@APIResponse(responseCode = "401", description = "Unauthorized") +@APIResponse(responseCode = "403", description = "Forbidden - insufficient scopes") +public Response createSecuredProduct(Product product) { + // Method implementation +} +---- + +The generated OpenAPI document includes comprehensive security information: + +[source, yaml] +---- +openapi: 3.1.0 +info: + title: Secured Product API + version: 1.0.0 + description: Product API with multiple security schemes +components: + securitySchemes: + apiKey: + type: apiKey + description: API Key authentication + name: X-API-Key + in: header + bearerAuth: + type: http + description: JWT Bearer token authentication + scheme: bearer + bearerFormat: JWT + oauth2: + type: oauth2 + description: OAuth2 authentication + flows: + authorizationCode: + authorizationUrl: https://example.com/oauth/authorize + tokenUrl: https://example.com/oauth/token + scopes: + read:products: Read product information + write:products: Modify product information +paths: + /api/products/{id}: + get: + summary: Get product by ID + description: Requires authentication + security: + - bearerAuth: [] + responses: + "200": + description: Product found + "401": + description: Unauthorized + "404": + description: Product not found + /api/products: + post: + summary: Create a new product + description: Requires OAuth2 write scope + security: + - oauth2: + - write:products + responses: + "201": + description: Product created + "401": + description: Unauthorized + "403": + description: Forbidden - insufficient scopes +---- + == Summary -By integrating the MicroProfile OpenAPI, developers can generate detailed, OpenAPI-compliant documentation automatically, fostering better understanding and interaction among services. By annotating `ProductResource` class, we generated API documentation as per Open API specification. This will ensure the services are readily discoverable, understandable, and usable, thereby accelerating development cycles and fostering a more robust and collaborative developer ecosystem. +By integrating MicroProfile OpenAPI 4.1, developers can generate detailed, OpenAPI v3.1-compliant documentation automatically, fostering better understanding and interaction among services. The new features in version 4.1, including enhanced support for Java Records, improved handling of Optional types, JSON Schema dialect specification, and comprehensive security scheme documentation, make it easier than ever to create robust, well-documented APIs. + +MicroProfile OpenAPI 4.1's alignment with OpenAPI v3.1 ensures that your API documentation leverages the latest standards in the API ecosystem. By annotating your `ProductResource` class and utilizing the new capabilities such as the Extensible interface methods, @Target annotations, and async operation support, you can generate comprehensive API documentation that ensures your services are readily discoverable, understandable, and usable. This accelerates development cycles and fosters a more robust and collaborative developer ecosystem.