Documenting Endpoints

Annotations

Annotation (io.swagger.v3.oas.annotations)WherePurpose
@Tagcontroller classGroup name and description
@OperationmethodSummary and description of the endpoint
@ApiResponsemethodPossible status codes and their meaning
@ParameterparameterDescribe a path/query parameter
@SchemaDTO / fieldField description and example value

A Documented Controller

@Tag(name = "Products", description = "Browse and manage the product catalogue")
@RestController
@RequestMapping("/api/products")
class ProductController {

    @Operation(summary = "Get one product", description = "Returns a product by its id.")
    @ApiResponse(responseCode = "200", description = "Product found")
    @ApiResponse(responseCode = "404", description = "No product with this id",
                 content = @Content(mediaType = "application/problem+json",
                                    schema = @Schema(implementation = ProblemDetail.class)))
    @GetMapping("/{id}")
    ProductResponse get(@Parameter(description = "Product id", example = "1") @PathVariable Long id) {
        return service.get(id);
    }
}

public record CreateProductRequest(
        @Schema(description = "Stock keeping unit", example = "ELE-1001") @NotNull @ValidSku String sku,
        @Schema(example = "Wireless Mouse") @NotBlank @Size(max = 120) String name,
        @Schema(example = "799.00") @NotNull @DecimalMin("0.01") BigDecimal price,
        @Schema(example = "120") @PositiveOrZero int stock,
        @Schema(example = "Electronics") @NotBlank String category) {}

How springdoc Finds Your Endpoints

Example

springdoc scans controller methods and their annotations with reflection when the app starts. The same idea, in a few lines:

import java.lang.annotation.*;
import java.lang.reflect.Method;
import java.util.*;

public class Main {
    public static void main(String[] args) {
        Method[] methods = ProductController.class.getDeclaredMethods();
        Arrays.sort(methods, Comparator.comparing(Method::getName));
        System.out.println("paths:");
        for (Method m : methods) {
            Endpoint e = m.getAnnotation(Endpoint.class);
            if (e == null) continue;
            System.out.println("  " + e.method() + " " + e.path());
            System.out.println("    summary: " + e.summary());
            System.out.println("    operationId: " + m.getName());
        }
    }
}

@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD)
@interface Endpoint { String method(); String path(); String summary(); }

class ProductController {
    @Endpoint(method = "GET", path = "/api/products", summary = "List products")
    void list() {}
    @Endpoint(method = "GET", path = "/api/products/{id}", summary = "Get one product")
    void get() {}
    @Endpoint(method = "POST", path = "/api/products", summary = "Create a product")
    void create() {}
    void helper() {}          // not an endpoint: ignored
}

Output

paths:
  POST /api/products
    summary: Create a product
    operationId: create
  GET /api/products/{id}
    summary: Get one product
    operationId: get
  GET /api/products
    summary: List products
    operationId: list