Swagger Core annotations library providing OpenAPI 3.x annotations for Java applications, enabling developers to document REST APIs with metadata annotations
—
This document covers advanced OpenAPI annotations including extensions, callbacks for asynchronous operations, webhooks, links between operations, and OpenAPI 3.1 enhanced features.
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.Extensions;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.callbacks.Callback;
import io.swagger.v3.oas.annotations.callbacks.Callbacks;
import io.swagger.v3.oas.annotations.Webhook;
import io.swagger.v3.oas.annotations.Webhooks;
import io.swagger.v3.oas.annotations.links.Link;
import io.swagger.v3.oas.annotations.links.LinkParameter;
import io.swagger.v3.oas.annotations.headers.Header;
import io.swagger.v3.oas.annotations.OpenAPI31;OpenAPI extensions allow you to add vendor-specific or custom properties to your API specification using the x- prefix convention.
@Operation(
summary = "Get pet by ID",
extensions = {
@Extension(name = "x-rate-limit", properties = {
@ExtensionProperty(name = "requests", value = "100"),
@ExtensionProperty(name = "period", value = "minute")
}),
@Extension(name = "x-cache-duration", properties = {
@ExtensionProperty(name = "seconds", value = "300")
})
}
)
@GetMapping("/pets/{id}")
public ResponseEntity<Pet> getPetById(@PathVariable Long id) {
return ResponseEntity.ok(petService.findById(id));
}@Schema(
description = "Pet information with enhanced metadata",
extensions = {
@Extension(name = "x-data-classification", properties = {
@ExtensionProperty(name = "level", value = "public"),
@ExtensionProperty(name = "retention", value = "7-years"),
@ExtensionProperty(name = "pii", value = "false")
}),
@Extension(name = "x-validation-rules", properties = {
@ExtensionProperty(name = "custom-validator", value = "PetNameValidator"),
@ExtensionProperty(name = "business-rules", value = "no-duplicate-names-per-owner")
}),
@Extension(name = "x-ui-hints", properties = {
@ExtensionProperty(name = "display-order", value = "name,category,status,tags"),
@ExtensionProperty(name = "required-highlight", value = "true"),
@ExtensionProperty(name = "collapse-advanced", value = "true")
})
}
)
public class Pet {
@Schema(
description = "Pet name",
extensions = {
@Extension(name = "x-input-validation", properties = {
@ExtensionProperty(name = "trim-whitespace", value = "true"),
@ExtensionProperty(name = "capitalize-first", value = "true")
})
}
)
private String name;
@Schema(
description = "Pet status",
extensions = {
@Extension(name = "x-state-machine", properties = {
@ExtensionProperty(name = "transitions", value = "available->pending,pending->sold,sold->available"),
@ExtensionProperty(name = "initial-state", value = "available")
})
}
)
private String status;
}@OpenAPIDefinition(
info = @Info(
title = "Pet Store API with Advanced Features",
version = "1.0.0",
extensions = {
@Extension(name = "x-api-capabilities", properties = {
@ExtensionProperty(name = "real-time", value = "webhooks,sse"),
@ExtensionProperty(name = "batch-operations", value = "supported"),
@ExtensionProperty(name = "async-processing", value = "callbacks")
}),
@Extension(name = "x-service-level", properties = {
@ExtensionProperty(name = "availability", value = "99.9%"),
@ExtensionProperty(name = "response-time-p95", value = "200ms"),
@ExtensionProperty(name = "throughput", value = "1000-rps")
})
}
),
extensions = {
@Extension(name = "x-monetization", properties = {
@ExtensionProperty(name = "pricing-model", value = "usage-based"),
@ExtensionProperty(name = "billing-cycle", value = "monthly"),
@ExtensionProperty(name = "free-tier", value = "1000-requests")
}),
@Extension(name = "x-compliance", properties = {
@ExtensionProperty(name = "gdpr", value = "compliant"),
@ExtensionProperty(name = "soc2", value = "type-2"),
@ExtensionProperty(name = "pci-dss", value = "level-1")
})
}
)@Server(
url = "https://api.petstore.io/v1",
description = "Production server with enhanced features",
extensions = {
@Extension(name = "x-server-capabilities", properties = {
@ExtensionProperty(name = "websockets", value = "supported"),
@ExtensionProperty(name = "server-sent-events", value = "supported"),
@ExtensionProperty(name = "graphql", value = "/graphql"),
@ExtensionProperty(name = "grpc", value = "api.petstore.io:443")
}),
@Extension(name = "x-infrastructure", properties = {
@ExtensionProperty(name = "provider", value = "aws"),
@ExtensionProperty(name = "region", value = "us-east-1"),
@ExtensionProperty(name = "load-balancer", value = "application"),
@ExtensionProperty(name = "cdn", value = "cloudfront")
})
}
)Define callback requests that your API will make to client-provided URLs, typically for asynchronous operations and event notifications.
@PostMapping("/pets/{id}/process")
@Operation(
summary = "Start pet processing",
description = "Initiates asynchronous pet processing and calls back when complete",
callbacks = @Callback(
name = "processingComplete",
callbackUrlExpression = "{$request.body#/callbackUrl}",
operation = @Operation(
method = "POST",
summary = "Processing completion notification",
requestBody = @RequestBody(
description = "Processing results",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ProcessingResult.class)
)
),
responses = {
@ApiResponse(
responseCode = "200",
description = "Callback received successfully"
),
@ApiResponse(
responseCode = "400",
description = "Invalid callback data"
)
}
)
)
)
public ResponseEntity<ProcessingRequest> startProcessing(
@PathVariable Long id,
@RequestBody ProcessingRequest request
) {
// Start async processing
ProcessingRequest response = petService.startProcessing(id, request);
return ResponseEntity.accepted().body(response);
}@PostMapping("/orders")
@Operation(
summary = "Create order with status notifications",
callbacks = {
@Callback(
name = "orderStatusUpdate",
callbackUrlExpression = "{$request.body#/webhooks.statusUpdate}",
operation = @Operation(
method = "POST",
summary = "Order status change notification",
requestBody = @RequestBody(
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = OrderStatusEvent.class)
)
),
responses = @ApiResponse(responseCode = "200", description = "Status update received")
)
),
@Callback(
name = "paymentProcessed",
callbackUrlExpression = "{$request.body#/webhooks.payment}",
operation = @Operation(
method = "POST",
summary = "Payment processing notification",
requestBody = @RequestBody(
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = PaymentEvent.class)
)
),
responses = @ApiResponse(responseCode = "200", description = "Payment notification received")
)
),
@Callback(
name = "deliveryUpdate",
callbackUrlExpression = "{$request.body#/webhooks.delivery}",
operation = @Operation(
method = "POST",
summary = "Delivery status notification",
requestBody = @RequestBody(
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = DeliveryEvent.class)
)
),
responses = @ApiResponse(responseCode = "200", description = "Delivery update received")
)
)
}
)@PostMapping("/subscriptions")
@Operation(
summary = "Create webhook subscription",
callbacks = @Callback(
name = "webhookEvent",
callbackUrlExpression = "{$request.body#/targetUrl}",
operation = @Operation(
method = "POST",
summary = "Webhook event notification",
description = "Called when subscribed events occur",
security = @SecurityRequirement(name = "webhook_signature"),
parameters = {
@Parameter(
name = "X-Webhook-Signature",
in = ParameterIn.HEADER,
description = "HMAC signature for webhook verification",
required = true,
schema = @Schema(type = "string")
),
@Parameter(
name = "X-Event-Type",
in = ParameterIn.HEADER,
description = "Type of event being delivered",
required = true,
schema = @Schema(type = "string", allowableValues = {
"pet.created", "pet.updated", "pet.deleted",
"order.placed", "order.shipped", "order.delivered"
})
),
@Parameter(
name = "X-Delivery-ID",
in = ParameterIn.HEADER,
description = "Unique delivery attempt identifier",
required = true,
schema = @Schema(type = "string", format = "uuid")
)
},
requestBody = @RequestBody(
description = "Event payload",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = WebhookEvent.class),
examples = {
@ExampleObject(
name = "petCreated",
summary = "Pet created event",
value = """
{
"eventType": "pet.created",
"timestamp": "2024-01-15T10:30:00Z",
"data": {
"id": 123,
"name": "Fluffy",
"status": "available"
}
}
"""
)
}
)
),
responses = {
@ApiResponse(
responseCode = "200",
description = "Webhook processed successfully"
),
@ApiResponse(
responseCode = "400",
description = "Invalid webhook payload"
),
@ApiResponse(
responseCode = "401",
description = "Invalid webhook signature"
)
}
)
)
)Document webhook endpoints that clients can implement to receive notifications from your API.
@Webhook(
name = "petUpdated",
operation = @Operation(
method = "POST",
summary = "Pet update notification",
description = "Called when a pet's information is updated",
requestBody = @RequestBody(
description = "Pet update event data",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = PetUpdateEvent.class)
)
),
responses = {
@ApiResponse(responseCode = "200", description = "Webhook acknowledged"),
@ApiResponse(responseCode = "410", description = "Webhook endpoint no longer exists")
}
)
)@Webhooks({
@Webhook(
name = "petLifecycle",
operation = @Operation(
summary = "Pet lifecycle events",
description = "Notifications for pet creation, updates, and deletion",
requestBody = @RequestBody(
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = PetLifecycleEvent.class),
examples = {
@ExampleObject(
name = "created",
summary = "Pet created",
value = """
{
"event": "pet.created",
"timestamp": "2024-01-15T10:30:00Z",
"pet": {
"id": 123,
"name": "New Pet",
"status": "available"
}
}
"""
),
@ExampleObject(
name = "updated",
summary = "Pet updated",
value = """
{
"event": "pet.updated",
"timestamp": "2024-01-15T10:35:00Z",
"pet": {
"id": 123,
"name": "Updated Pet",
"status": "sold"
},
"changes": ["name", "status"]
}
"""
)
}
)
)
)
),
@Webhook(
name = "orderStatus",
operation = @Operation(
summary = "Order status changes",
description = "Notifications when order status changes",
requestBody = @RequestBody(
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = OrderStatusEvent.class)
)
)
)
),
@Webhook(
name = "inventoryAlert",
operation = @Operation(
summary = "Inventory level alerts",
description = "Notifications when inventory reaches threshold levels",
requestBody = @RequestBody(
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = InventoryAlert.class)
)
)
)
)
})@Webhook(
name = "secureWebhook",
operation = @Operation(
summary = "Secure webhook with authentication",
description = "Webhook with signature verification and custom headers",
parameters = {
@Parameter(
name = "X-Webhook-Signature",
in = ParameterIn.HEADER,
required = true,
description = "HMAC-SHA256 signature of the payload",
schema = @Schema(type = "string"),
example = "sha256=a8b7c6d5e4f3g2h1..."
),
@Parameter(
name = "X-Event-ID",
in = ParameterIn.HEADER,
required = true,
description = "Unique event identifier for deduplication",
schema = @Schema(type = "string", format = "uuid")
),
@Parameter(
name = "X-Retry-Count",
in = ParameterIn.HEADER,
required = false,
description = "Number of delivery attempts (starts at 0)",
schema = @Schema(type = "integer", minimum = "0", maximum = "5")
)
},
requestBody = @RequestBody(
description = "Signed webhook payload",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = SecureWebhookEvent.class)
)
),
responses = {
@ApiResponse(
responseCode = "200",
description = "Webhook processed successfully"
),
@ApiResponse(
responseCode = "401",
description = "Invalid signature",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = WebhookError.class)
)
),
@ApiResponse(
responseCode = "409",
description = "Duplicate event (already processed)"
),
@ApiResponse(
responseCode = "422",
description = "Valid signature but invalid payload"
)
}
)
)public @interface Webhook {
String name(); // Required - Name of the webhook
Operation operation(); // Required - Operation definition for the webhook
}
public @interface Webhooks {
Webhook[] value() default {}; // Array of Webhook annotations
}Define relationships between operations, allowing clients to navigate from one operation's response to related operations.
@PostMapping("/pets")
@ApiResponse(
responseCode = "201",
description = "Pet created successfully",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = Pet.class)
),
links = {
@Link(
name = "GetPetById",
description = "Retrieve the newly created pet",
operationId = "getPetById",
parameters = @LinkParameter(
name = "id",
expression = "$response.body#/id"
)
),
@Link(
name = "UpdatePet",
description = "Update the newly created pet",
operationId = "updatePet",
parameters = @LinkParameter(
name = "id",
expression = "$response.body#/id"
)
),
@Link(
name = "DeletePet",
description = "Delete the newly created pet",
operationId = "deletePet",
parameters = @LinkParameter(
name = "id",
expression = "$response.body#/id"
)
)
}
)
public ResponseEntity<Pet> createPet(@RequestBody Pet pet) {
Pet createdPet = petService.create(pet);
return ResponseEntity.status(HttpStatus.CREATED).body(createdPet);
}@GetMapping("/users/{userId}/pets")
@ApiResponse(
responseCode = "200",
description = "User's pets retrieved",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = PetListResponse.class)
),
links = {
@Link(
name = "GetUserProfile",
description = "Get full user profile",
operationId = "getUserById",
parameters = @LinkParameter(
name = "id",
expression = "$request.path.userId"
)
),
@Link(
name = "CreatePetForUser",
description = "Create a new pet for this user",
operationId = "createPetForUser",
parameters = {
@LinkParameter(
name = "userId",
expression = "$request.path.userId"
)
}
),
@Link(
name = "GetPetDetails",
description = "Get details for any pet in the list",
operationId = "getPetById",
parameters = @LinkParameter(
name = "id",
expression = "$response.body#/pets/*/id"
)
),
@Link(
name = "GetUserOrders",
description = "Get orders placed by this user",
operationId = "getUserOrders",
parameters = @LinkParameter(
name = "userId",
expression = "$request.path.userId"
)
)
}
)@GetMapping("/orders/{id}")
@ApiResponse(
responseCode = "200",
description = "Order details",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = Order.class)
),
links = {
@Link(
name = "CancelOrder",
description = "Cancel this order (if cancellable)",
operationId = "cancelOrder",
parameters = @LinkParameter(
name = "id",
expression = "$response.body#/id"
),
extensions = {
@Extension(name = "x-conditional", properties = {
@ExtensionProperty(name = "condition", value = "$response.body#/status == 'pending'"),
@ExtensionProperty(name = "description", value = "Only available for pending orders")
})
}
),
@Link(
name = "TrackShipment",
description = "Track order shipment",
operationId = "getShipmentTracking",
parameters = @LinkParameter(
name = "trackingNumber",
expression = "$response.body#/trackingNumber"
),
extensions = {
@Extension(name = "x-conditional", properties = {
@ExtensionProperty(name = "condition", value = "$response.body#/status == 'shipped'"),
@ExtensionProperty(name = "description", value = "Only available for shipped orders")
})
}
),
@Link(
name = "RequestReturn",
description = "Request return for delivered order",
operationId = "createReturnRequest",
parameters = @LinkParameter(
name = "orderId",
expression = "$response.body#/id"
),
extensions = {
@Extension(name = "x-conditional", properties = {
@ExtensionProperty(name = "condition", value = "$response.body#/status == 'delivered'"),
@ExtensionProperty(name = "timeLimit", value = "30 days from delivery")
})
}
)
}
)Leverage enhanced OpenAPI 3.1 capabilities including improved JSON Schema support, conditional schemas, and enhanced validation.
@Schema(
description = "Enhanced pet schema with OpenAPI 3.1 features",
types = {"object", "null"}, // Multiple types
$id = "https://api.petstore.io/schemas/pet.json",
$schema = "https://json-schema.org/draft/2020-12/schema"
)
@OpenAPI31 // Marker for OpenAPI 3.1 specific features
public class EnhancedPet {
@Schema(
description = "Pet identifier with enhanced validation",
types = {"integer", "string"}, // Can be either integer or string ID
pattern = "^(\\d+|[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})$"
)
@OpenAPI31
private String id;
@Schema(
description = "Pet metadata with conditional requirements",
_if = @Schema(
properties = @SchemaProperty(
name = "type",
schema = @Schema(allowableValues = "premium")
)
),
_then = @Schema(
required = {"certification", "insurance"},
properties = {
@SchemaProperty(
name = "certification",
schema = @Schema(type = "string", minLength = 1)
),
@SchemaProperty(
name = "insurance",
schema = @Schema(type = "object", required = {"provider", "policyNumber"})
)
}
)
)
@OpenAPI31
private Map<String, Object> metadata;
@Schema(
description = "Pet images with enhanced array validation",
prefixItems = {
@Schema(description = "Primary image", format = "uri"),
@Schema(description = "Thumbnail image", format = "uri")
},
minContains = 1,
maxContains = 3,
contains = @Schema(
description = "Must contain at least one high-resolution image",
properties = @SchemaProperty(
name = "resolution",
schema = @Schema(allowableValues = "high")
)
)
)
@OpenAPI31
private List<PetImage> images;
}@Schema(
description = "Payment method with conditional validation",
discriminatorProperty = "type"
)
@OpenAPI31
public class PaymentMethod {
@Schema(description = "Payment type")
private String type;
@Schema(
description = "Payment details with type-specific validation",
_if = @Schema(
properties = @SchemaProperty(
name = "type",
schema = @Schema(allowableValues = "credit_card")
)
),
_then = @Schema(
required = {"cardNumber", "expiryDate", "cvv"},
properties = {
@SchemaProperty(
name = "cardNumber",
schema = @Schema(type = "string", pattern = "^[0-9]{13,19}$")
),
@SchemaProperty(
name = "expiryDate",
schema = @Schema(type = "string", pattern = "^(0[1-9]|1[0-2])\\/[0-9]{2}$")
),
@SchemaProperty(
name = "cvv",
schema = @Schema(type = "string", pattern = "^[0-9]{3,4}$")
)
}
),
_else = @Schema(
_if = @Schema(
properties = @SchemaProperty(
name = "type",
schema = @Schema(allowableValues = "paypal")
)
),
_then = @Schema(
required = {"paypalEmail"},
properties = @SchemaProperty(
name = "paypalEmail",
schema = @Schema(type = "string", format = "email")
)
),
_else = @Schema(
_if = @Schema(
properties = @SchemaProperty(
name = "type",
schema = @Schema(allowableValues = "bank_transfer")
)
),
_then = @Schema(
required = {"accountNumber", "routingNumber"},
properties = {
@SchemaProperty(
name = "accountNumber",
schema = @Schema(type = "string", minLength = 8, maxLength = 17)
),
@SchemaProperty(
name = "routingNumber",
schema = @Schema(type = "string", pattern = "^[0-9]{9}$")
)
}
)
)
)
)
@OpenAPI31
private Map<String, Object> details;
}@Content(
mediaType = "application/json",
oneOf = {
@Schema(implementation = StandardUser.class),
@Schema(implementation = PremiumUser.class),
@Schema(implementation = EnterpriseUser.class)
},
unevaluatedProperties = @Schema(
description = "Additional properties not covered by oneOf schemas",
additionalProperties = Schema.AdditionalPropertiesValue.FALSE
)
)
@OpenAPI31
public class UserResponse {
// Base user properties that apply to all user types
}
@Schema(
description = "Dynamic configuration with pattern properties",
patternProperties = {
@PatternProperty(
pattern = "^feature_[a-z]+$",
schema = @Schema(
type = "object",
properties = {
@SchemaProperty(name = "enabled", schema = @Schema(type = "boolean")),
@SchemaProperty(name = "config", schema = @Schema(type = "object"))
}
)
),
@PatternProperty(
pattern = "^metric_[a-z_]+$",
schema = @Schema(
type = "number",
minimum = "0"
)
)
},
unevaluatedProperties = @Schema(additionalProperties = Schema.AdditionalPropertiesValue.FALSE)
)
@OpenAPI31
public class DynamicConfiguration {
// Static configuration properties
@Schema(description = "Configuration version")
private String version;
@Schema(description = "Last updated timestamp")
private Instant updatedAt;
// Dynamic properties validated by pattern properties
}@Schema(
description = "File data with encoding information",
contentEncoding = "base64",
contentMediaType = "image/jpeg",
type = "string"
)
@OpenAPI31
public class EncodedFileData {
@Schema(
description = "Base64 encoded image data",
contentEncoding = "base64",
contentMediaType = "image/jpeg"
)
private String imageData;
@Schema(
description = "Compressed text data",
contentEncoding = "gzip",
contentMediaType = "text/plain"
)
private String compressedText;
@Schema(
description = "Binary file data",
contentEncoding = "binary",
contentMediaType = "application/octet-stream"
)
private String binaryData;
}@RestController
@OpenAPIDefinition(
info = @Info(
title = "Advanced Pet Store API",
version = "3.1.0",
extensions = {
@Extension(name = "x-api-features", properties = {
@ExtensionProperty(name = "webhooks", value = "supported"),
@ExtensionProperty(name = "callbacks", value = "supported"),
@ExtensionProperty(name = "openapi-version", value = "3.1")
})
}
)
)
@Webhooks({
@Webhook(
name = "petStatusChanged",
operation = @Operation(
summary = "Pet status change notification",
requestBody = @RequestBody(
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = PetStatusEvent.class)
)
)
)
)
})
public class AdvancedPetStoreController {
@PostMapping("/pets/async-process")
@Operation(
summary = "Start asynchronous pet processing",
extensions = {
@Extension(name = "x-processing-type", properties = {
@ExtensionProperty(name = "type", value = "asynchronous"),
@ExtensionProperty(name = "estimated-duration", value = "5-10 minutes")
})
},
callbacks = @Callback(
name = "processingComplete",
callbackUrlExpression = "{$request.body#/callbackUrl}",
operation = @Operation(
method = "POST",
summary = "Processing completion callback",
requestBody = @RequestBody(
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ProcessingResult.class)
)
)
)
)
)
@ApiResponse(
responseCode = "202",
description = "Processing started",
content = @Content(
schema = @Schema(implementation = ProcessingRequest.class)
),
links = {
@Link(
name = "CheckStatus",
operationId = "getProcessingStatus",
parameters = @LinkParameter(
name = "processId",
expression = "$response.body#/processId"
)
),
@Link(
name = "CancelProcessing",
operationId = "cancelProcessing",
parameters = @LinkParameter(
name = "processId",
expression = "$response.body#/processId"
)
)
}
)
public ResponseEntity<ProcessingRequest> startAsyncProcessing(
@RequestBody @OpenAPI31 AsyncProcessingRequest request
) {
ProcessingRequest result = petService.startAsyncProcessing(request);
return ResponseEntity.accepted().body(result);
}
}Install with Tessl CLI
npx tessl i tessl/maven-io-swagger-core-v3--swagger-annotations