This tutorial demonstrates that OfficeFloor REST endpoints declared as YAML files appear automatically in the OpenAPI specification generated by springdoc-openapi. No additional configuration is needed — adding the springdoc dependency is sufficient.
The tutorial exposes a simple product catalogue with two endpoints:
GET /product — returns a list of all productsGET /product/{id} — returns a single product by identifierAdd springdoc-openapi-starter-webmvc-ui alongside the OfficeFloor starter:
<dependency> <groupId>net.officefloor.springboot</groupId> <artifactId>officefloor-rest-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>2.8.6</version> </dependency>
That is the only change required — OfficeFloor's built-in SpringDoc integration registers all YAML-declared endpoints with the OpenApiCustomizer at start-up.
@SpringBootApplication
public class SpringRestOpenApiApplication {
public static void main(String[] args) {
SpringApplication.run(SpringRestOpenApiApplication.class, args);
}
}
@Schema annotationsSwagger annotations on the response class populate the component schema in the generated spec:
@Data
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "A product in the catalogue")
public class Product {
@Schema(description = "Unique product identifier")
private Long id;
@Schema(description = "Product name")
private String name;
@Schema(description = "Product price in cents")
private int priceCents;
}
A plain Spring @Service holds the in-memory catalogue:
@Service
public class ProductCatalogueService {
private final List<Product> products = List.of(
new Product(1L, "Widget", 999),
new Product(2L, "Gadget", 1999),
new Product(3L, "Doohickey", 4999));
public List<Product> findAll() {
return products;
}
public Product findById(long id) {
return products.stream()
.filter(p -> p.getId() == id)
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Product not found: " + id));
}
}
@Operation@Operation on the service method populates the operation summary in the spec. The ProductCatalogueService is injected by type — no annotation needed on that parameter:
public class ListProductsService {
@Operation(summary = "List all products")
public void service(ProductCatalogueService catalogue, ObjectResponse<List<Product>> response) {
response.send(catalogue.findAll());
}
}
public class GetProductService {
@Operation(summary = "Get product by ID")
public void service(@PathVariable(name = "id") Long id,
ProductCatalogueService catalogue,
ObjectResponse<Product> response) {
response.send(catalogue.findById(id));
}
}
Each endpoint is declared as a YAML file under officefloor/rest/:
officefloor/rest/
├── product.GET.yml → GET /product
└── product/
└── {id}.GET.yml → GET /product/{id}service: class: net.officefloor.tutorial.springrestopenapi.ListProductsService
service: class: net.officefloor.tutorial.springrestopenapi.GetProductService
Tests verify both endpoint behaviour and that the paths appear in the live OpenAPI JSON at GET /v3/api-docs:
@SpringBootTest
@AutoConfigureMockMvc
public class SpringRestOpenApiTest {
@Autowired
private MockMvc mvc;
@Autowired
private ObjectMapper mapper;
@Test
public void listProducts() throws Exception {
mvc.perform(get("/product").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].name").value("Widget"))
.andExpect(jsonPath("$[1].name").value("Gadget"));
}
@Test
public void getProductById() throws Exception {
mvc.perform(get("/product/1").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Widget"))
.andExpect(jsonPath("$.priceCents").value(999));
}
@Test
public void openApiSpecIncludesListEndpoint() throws Exception {
mvc.perform(get("/v3/api-docs"))
.andExpect(status().isOk())
.andExpect(content().string(containsString("/product")));
}
@Test
public void openApiSpecIncludesGetByIdEndpoint() throws Exception {
mvc.perform(get("/v3/api-docs"))
.andExpect(status().isOk())
.andExpect(content().string(containsString("/product/{id}")));
}
}
The Swagger UI is available at http://localhost:8080/swagger-ui.html when the application is running.
The Spring REST Thymeleaf tutorial demonstrates server-side HTML rendering with Thymeleaf from OfficeFloor REST service methods.