OpenAPI & springdoc-openapi — auto-generate API doc từ controller
OpenAPI 3.1 là chuẩn JSON/YAML mô tả REST API contract. springdoc-openapi tự scan @RestController và sinh spec, expose Swagger UI tại /swagger-ui.html. Bài này giải thích cơ chế quét annotation, customize qua @Operation/@Schema, và global metadata — đặc biệt tại sao spec auto-generated là lựa chọn đúng.
TL;DR: OpenAPI Specification 3.1 là file JSON/YAML mô tả toàn bộ contract REST API — endpoint, method, parameter, body schema, response. springdoc-openapi quét @RestController annotation lúc startup, xây dựng object OpenAPI trong bộ nhớ, rồi expose spec tại /v3/api-docs và Swagger UI tại /swagger-ui.html. Code là nguồn sự thật duy nhất — spec auto-generated đảm bảo doc không bao giờ lệch implementation. Customize qua @Operation, @Schema, @ApiResponse; global metadata qua @Bean OpenAPI.
Backend viết xong OrderController 20 endpoint. Frontend hỏi: "Endpoint nào? Body field gì? Status code nào trả về?" Câu trả lời truyền thống là viết tay Confluence, nhưng Confluence luôn lệch code sau vài sprint. OpenAPI giải quyết bằng cách để code tự mô tả chính nó — springdoc-openapi quét annotation Spring MVC đã có sẵn và sinh ra spec mà không cần viết thêm gì.
Bài này tập trung một mục tiêu: hiểu cơ chế springdoc quét và sinh spec, biết customize annotation cơ bản, và biết cấu hình global metadata. Bài tiếp theo (Security scheme, groups & API-first) đào sâu JWT security scheme, grouped API, và workflow API-first.
1. OpenAPI Specification 3.1 — chuẩn mô tả contract
OpenAPI Specification (OAS) — chuẩn JSON/YAML mô tả REST API, phiên bản hiện tại 3.1 (2021). Trước 2016 tên là Swagger Specification; từ 2016 trao cho Linux Foundation, đổi tên thành OpenAPI.
Spec mô tả toàn bộ contract — không phải guide, không phải tutorial, mà là machine-readable definition:
openapi: 3.1.0
info:
title: Order API
version: 1.0.0
paths:
/orders/{id}:
get:
operationId: getOrder
parameters:
- name: id
in: path
required: true
schema:
type: integer
format: int64
responses:
'200':
description: Order found
content:
application/json:
schema:
$ref: '#/components/schemas/OrderDto'
'404':
description: Order not found
components:
schemas:
OrderDto:
type: object
properties:
id: { type: integer, format: int64 }
customer: { type: string }
total: { type: number }
Spec là fuel cho cả hệ sinh thái tool:
| Tool | Dùng để |
|---|---|
| Swagger UI | Render spec thành interactive HTML, "Try it out" trực tiếp |
| OpenAPI Generator | Sinh client SDK 50+ ngôn ngữ (TypeScript, Python, Kotlin) |
| Postman / Bruno | Import spec, tự tạo collection |
| Spectral | Lint spec, bắt lỗi design sớm |
| Prism / Mockoon | Chạy spec như mock server |
Vì sao spec auto-generated thắng doc viết tay?
Doc viết tay Confluence có vấn đề cốt lõi: hai nguồn sự thật — code và doc, và chúng không tự đồng bộ. Sau khi team refactor đổi tên field orderDate thành createdAt, doc vẫn nói orderDate. Frontend gọi sai field, nhận 400, mất nửa ngày debug.
Auto-generated spec từ springdoc loại bỏ nguồn sự thật thứ hai: spec sinh trực tiếp từ code đang chạy. Code thay đổi thì spec thay đổi theo ngay ở lần request tiếp theo. Không bao giờ lệch.
Code Spring controller là nguồn sự thật duy nhất cho cả implementation lẫn documentation. springdoc-openapi đọc annotation đã viết sẵn — không phải viết lại thông tin dưới dạng khác.
2. springdoc-openapi — cơ chế quét và sinh spec
Thêm một dependency vào pom.xml:
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.6.0</version>
</dependency>
Không cần config gì thêm. Spring Boot autoconfiguration của springdoc tự kích hoạt, expose:
/v3/api-docs— spec dạng JSON/v3/api-docs.yaml— spec dạng YAML/swagger-ui.html— Swagger UI interactive
Cơ chế bên dưới — springdoc quét annotation ra sao
springdoc-openapi chạy một ApplicationListener nhận event WebApplicationContext ready. Tại thời điểm này, toàn bộ bean Spring đã được khởi tạo (xem BeanFactory vs ApplicationContext để hiểu pha startup). springdoc lấy danh sách bean, lọc những bean có annotation @RestController, rồi đọc reflection metadata của chúng (bảng đầy đủ ở mục dưới).

Điểm cần nhớ nằm ở hộp giữa: không có file spec nào trên đĩa để mà lệch. Object OpenAPI dựng lại từ đầu mỗi lần app khởi động, từ chính annotation đang chạy — đó mới là lý do spec không bao giờ cũ, chứ không phải vì ai đó nhớ cập nhật nó.
Điểm then chốt: springdoc không phân tích bytecode mà đọc reflection — annotation là metadata tồn tại ở runtime, Spring annotation (@GetMapping, @RequestParam) và Bean Validation annotation (@NotNull, @Size) đều available qua Class.getAnnotations(). Đây là lý do auto-detection work mà không cần xử lý compile-time.
Những gì springdoc tự detect được
springdoc đọc và map sang spec:
| Annotation Spring/Jakarta | Sinh ra field nào trong spec |
|---|---|
@RestController + @RequestMapping | Path item trong paths |
@GetMapping("/orders/{id}") | GET /orders/{id} operation |
@PathVariable Long id | Parameter in: path, required: true |
@RequestParam(required=false) | Parameter in: query, required: false |
@RequestBody OrderRequest | requestBody với schema ref tới DTO |
@ResponseStatus(CREATED) | Response code 201 |
@NotNull, @Size(min=2) | Schema constraint required, minLength: 2 |
Return type ResponseEntity<OrderDto> | Response 200 với schema |
Khoảng 80% thông tin auto-detect đủ. 20% còn lại cần annotation bổ sung (section 3).
Sau khi add dependency và chạy app, mở http://localhost:8080/swagger-ui.html:
Order API (1.0.0)
├── Orders
│ ├── GET /api/v1/orders List orders
│ ├── GET /api/v1/orders/{id} Get order by ID
│ ├── POST /api/v1/orders Create order
│ ├── PUT /api/v1/orders/{id} Update order
│ └── DELETE /api/v1/orders/{id} Delete order
└── Schemas
├── OrderRequest
├── OrderDto
└── ProblemDetail
Click endpoint → expand → "Try it out" → fill parameter → Execute. Response hiển thị status code, header, body. Tiện hơn Postman cho onboard dev mới vì không cần cấu hình riêng.
3. Customize spec — annotation bổ sung
20% thông tin auto-detection không capture được là những thứ Java type system không encode: description nghiệp vụ, example value, semantic của từng response code. Dùng annotation từ package io.swagger.v3.oas.annotations.
@Operation — mô tả endpoint
@Operation(
summary = "Get order by ID",
description = "Retrieves full order details including items. Returns 404 if order ID does not exist in this tenant.",
tags = {"Orders"}
)
@GetMapping("/orders/{id}")
public OrderDto get(@PathVariable Long id) { ... }
summary xuất hiện trong list endpoint. description hiện khi expand. tags group endpoint thành section riêng trong UI — quan trọng khi controller có nhiều endpoint.
@Parameter — mô tả parameter
@GetMapping("/orders")
public List<OrderDto> list(
@Parameter(description = "Filter by order status", example = "ACTIVE")
@RequestParam(required = false) OrderStatus status,
@Parameter(description = "Page number, 0-indexed", example = "0")
@RequestParam(defaultValue = "0") int page,
@Parameter(description = "Page size (1-100)", example = "20")
@RequestParam(defaultValue = "20") int size
) { ... }
example là trường quan trọng nhất — user thấy ngay value hợp lệ, không phải đoán format.
@Schema — mô tả DTO field
@Schema(description = "Order creation request")
public record OrderRequest(
@Schema(description = "Customer full name", example = "Nguyen Van A", minLength = 2, maxLength = 100)
@NotBlank
String customer,
@Schema(description = "Order total in VND, must be positive", example = "299000", minimum = "1")
@NotNull @Positive
BigDecimal total,
@Schema(description = "Requested delivery date (ISO 8601 date)", example = "2026-07-15")
@NotNull @Future
LocalDate deliveryDate
) {}
@Schema trên record component map 1-1 với properties trong spec JSON. example cực kỳ quan trọng — "Try it out" pre-fill value này.
@ApiResponse — explicit response code
@Operation(summary = "Create order")
@ApiResponses({
@ApiResponse(responseCode = "201", description = "Order created successfully",
content = @Content(schema = @Schema(implementation = OrderDto.class))),
@ApiResponse(responseCode = "400", description = "Validation failed — check errors array",
content = @Content(schema = @Schema(implementation = ProblemDetail.class))),
@ApiResponse(responseCode = "409", description = "Duplicate order within 5 minutes"),
@ApiResponse(responseCode = "422", description = "Business rule violation")
})
@PostMapping("/orders")
@ResponseStatus(HttpStatus.CREATED)
public OrderDto create(@Valid @RequestBody OrderRequest req) { ... }
springdoc auto-detect 201 từ @ResponseStatus. Nhưng 409 và 422 không có annotation Spring tương ứng — phải khai báo explicit. Quan trọng hơn: description phân biệt semantic của từng code.
Mỗi annotation map thẳng sang một field trong OpenAPI spec:
| Annotation | Field sinh ra trong spec |
|---|---|
@Operation(summary, description, tags) | paths./orders.get.summary + .tags |
@Parameter(description, example) | paths./orders.get.parameters — description + example |
@Schema(description, example, minLength) | components.schemas.OrderRequest.properties |
@ApiResponse(responseCode, description) | paths./orders.post.responses.201 / .400 / .409 |
Pitfall: Quên @Schema(example = ...) trên DTO
❌ Không có example:
public record OrderRequest(
@NotBlank String customer,
@NotNull @Positive BigDecimal total
) {}
Swagger UI "Try it out" hiển thị "customer": "string", "total": 0 — placeholder vô nghĩa. User phải tự biết format.
✅ Có example:
public record OrderRequest(
@Schema(example = "Nguyen Van A") @NotBlank String customer,
@Schema(example = "299000") @NotNull @Positive BigDecimal total
) {}
UI hiển thị "customer": "Nguyen Van A", "total": 299000 — user copy và chỉnh sửa, tiết kiệm thời gian onboard.
4. Global metadata — cấu hình info và server
Customize title, version, server list qua @Bean OpenAPI:
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("TaskFlow API")
.version("v1.0")
.description("REST API cho TaskFlow project management platform")
.contact(new Contact()
.name("OLHub Engineering")
.email("[email protected]"))
.license(new License()
.name("Apache 2.0")
.url("https://www.apache.org/licenses/LICENSE-2.0")))
.servers(List.of(
new Server().url("http://localhost:8080").description("Local dev"),
new Server().url("https://staging.olhub.org").description("Staging"),
new Server().url("https://api.olhub.org").description("Production")
));
}
}
servers list xuất hiện trong dropdown phía trên Swagger UI — user chọn môi trường trước khi "Try it out", request tự đi đúng URL.
Ngoài Java config, nhiều property cấu hình qua application.yml:
springdoc:
api-docs:
path: /v3/api-docs # default
enabled: true
swagger-ui:
path: /swagger-ui.html # default
operationsSorter: method # sort theo HTTP method trong UI
tagsSorter: alpha # sort tag theo alphabet
tryItOutEnabled: true # bat Try it out mac dinh
filter: true # thanh search endpoint trong UI
packages-to-scan: com.olhub.api # chi quet package nay
paths-to-match: /api/** # chi match URL pattern nay
show-actuator: false # loai actuator endpoint khoi spec
packages-to-scan và paths-to-match quan trọng cho app lớn — tránh scan toàn bộ classpath, bao gồm endpoint nội bộ không nên expose.
Vì sao không dùng springdoc.api-docs.enabled: false ở dev?
Một số team disable Swagger UI từ sợ lộ thông tin, nhưng làm vậy ở môi trường dev mất đi toàn bộ lợi ích. Pattern đúng:
# application-dev.yml
springdoc:
api-docs:
enabled: true
swagger-ui:
enabled: true
# application-prod.yml
springdoc:
api-docs:
enabled: false
swagger-ui:
enabled: false
Hoặc auth-protect endpoint spec trong production (detail ở bài tiếp theo).
Liên hệ các bài khác
Bài này là nền cho các bài liên quan trong course:
- @RestController & mapping: springdoc đọc
@GetMapping,@PostMapping,@PathVariabletrên controller để sinh path item trong spec — annotation bài đó học là annotation springdoc quét ở bài này. Hiểu rõ mapping thì biết spec sinh ra từ đâu. - Validation với
@Validvà@Validated(bài trước): annotation@NotNull,@Size,@Mintrên DTO vừa trigger validation, vừa được springdoc đọc để sinh schema constraint trong spec — một annotation, hai công dụng. - Exception handling & ProblemDetail (bài trước):
@RestControllerAdvicehandler của bài đó được springdoc detect và sinh error response schema trong spec — đặc biệtProblemDetailclass. - Security scheme, groups & API-first (bài tiếp theo): sau khi nắm cơ chế cơ bản ở bài này, bài đó đào sâu JWT
@SecurityScheme,GroupedOpenApicho multi-API, và workflow API-first với OpenAPI Generator.
Tóm tắt
- OpenAPI Specification 3.1 là chuẩn JSON/YAML mô tả REST API contract — machine-readable, fuel cho cả hệ sinh thái tool (Swagger UI, Generator, Postman, Spectral).
- Vì sao auto-generated thắng doc viết tay: loại bỏ nguồn sự thật thứ hai, code thay đổi thì spec thay đổi theo ngay, không bao giờ lệch.
- springdoc-openapi: thêm 1 dependency, Boot autoconfig tự kích hoạt. Quét
@RestControllerqua reflection lúcContextRefreshed, xâyOpenAPIobject, expose/v3/api-docs+/swagger-ui.html. - Auto-detect: controller annotation, parameter binding, Bean Validation constraint, return type,
@ResponseStatus. - Customize annotation từ
io.swagger.v3.oas.annotations:@Operation(summary/description/tags),@Parameter(description/example),@Schema(description/example/constraint),@ApiResponse(explicit response code + semantic). - Pitfall quan trọng nhất: thiếu
@Schema(example = ...)trên DTO — Swagger UI "Try it out" không dùng được vì placeholder vô nghĩa. - Global metadata qua
@Bean OpenAPI— title, version, contact, server list với environment dropdown.
Tự kiểm tra
- Q1springdoc-openapi quét
@RestControllervà sinh spec vào lúc nào trong vòng đời Spring Boot? Vì sao không phải lúc compile? - Q2Kể 3 loại thông tin springdoc auto-detect được từ controller và 3 loại cần khai báo thủ công qua annotation. Vì sao 20% còn lại không auto được?
- Q3
@Schema(example = "299000")trên field DTO có tác dụng gì trong Swagger UI? Điều gì xảy ra nếu thiếu nó? - Q4Một endpoint có
@ResponseStatus(HttpStatus.CREATED)và némOrderNotFoundExceptionxử lý bởi@RestControllerAdvice. springdoc tự sinh response nào? Response nào cần khai báo thủ công? - Q5
springdoc.packages-to-scanvàspringdoc.paths-to-matchdùng để làm gì? Vì sao cần giới hạn scope quét thay vì để quét toàn bộ?
Bài tiếp theo: Security scheme, groups & API-first
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên