[spring]addition: api response examples + spring generator generates response examples (#17610, #16051) (#20933)

* addition: api response examples; spring's api.mustache generates response examples

* update samples

* added 2 sample configs; adsjuted to only generate examples for application/json

* added test

* remove accidentally added files

* small cleanup

* add new samples to workflow

---------

Co-authored-by: GlobeDaBoarder <glebivashyn@gmail.com>
Co-authored-by: William Cheng <wing328hk@gmail.com>
This commit is contained in:
martin-mfg
2025-04-26 18:10:09 +02:00
committed by GitHub
parent 35ba0414fb
commit 9f3a73f81a
129 changed files with 5949 additions and 4 deletions

View File

@@ -0,0 +1,23 @@
# OpenAPI Generator Ignore
# Generated by openapi-generator https://github.com/openapitools/openapi-generator
# Use this file to prevent files from being overwritten by the generator.
# The patterns follow closely to .gitignore or .dockerignore.
# As an example, the C# client generator defines ApiClient.cs.
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
#ApiClient.cs
# You can match any string of characters against a directory, file or extension with a single asterisk (*):
#foo/*/qux
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
#foo/**/qux
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
# You can also negate patterns with an exclamation (!).
# For example, you can ignore all files in a docs folder with the file extension .md:
#docs/*.md
# Then explicitly reverse the ignore rule for a single file:
#!docs/README.md

View File

@@ -0,0 +1,14 @@
README.md
pom.xml
src/main/java/org/openapitools/OpenApiGeneratorApplication.java
src/main/java/org/openapitools/RFC3339DateFormat.java
src/main/java/org/openapitools/api/ApiUtil.java
src/main/java/org/openapitools/api/DogsApi.java
src/main/java/org/openapitools/api/DogsApiDelegate.java
src/main/java/org/openapitools/configuration/HomeController.java
src/main/java/org/openapitools/configuration/SpringDocConfiguration.java
src/main/java/org/openapitools/model/Dog.java
src/main/java/org/openapitools/model/Error.java
src/main/resources/application.properties
src/main/resources/openapi.yaml
src/test/java/org/openapitools/OpenApiGeneratorApplicationTests.java

View File

@@ -0,0 +1 @@
7.13.0-SNAPSHOT

View File

@@ -0,0 +1,21 @@
# OpenAPI generated server
Spring Boot Server
## Overview
This server was generated by the [OpenAPI Generator](https://openapi-generator.tech) project.
By using the [OpenAPI-Spec](https://openapis.org), you can easily generate a server stub.
This is an example of building a OpenAPI-enabled server in Java using the SpringBoot framework.
The underlying library integrating OpenAPI to Spring Boot is [springdoc](https://springdoc.org).
Springdoc will generate an OpenAPI v3 specification based on the generated Controller and Model classes.
The specification is available to download using the following url:
http://localhost:8080/v3/api-docs/
Start your server as a simple java application
You can view the api documentation in swagger-ui by pointing to
http://localhost:8080/swagger-ui.html
Change default port value in application.properties

View File

@@ -0,0 +1,97 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.openapitools</groupId>
<artifactId>springboot-api-response-examples</artifactId>
<packaging>jar</packaging>
<name>springboot-api-response-examples</name>
<version>1.0.0</version>
<properties>
<java.version>17</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<springdoc.version>2.6.0</springdoc.version>
<swagger-ui.version>5.17.14</swagger-ui.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<repositories>
<repository>
<id>repository.spring.milestone</id>
<name>Spring Milestone Repository</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestones</id>
<url>https://repo.spring.io/milestone</url>
</pluginRepository>
</pluginRepositories>
<build>
<sourceDirectory>src/main/java</sourceDirectory>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
</dependency>
<!--SpringDoc dependencies -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<!-- @Nullable annotation -->
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<version>3.0.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>org.openapitools</groupId>
<artifactId>jackson-databind-nullable</artifactId>
<version>0.2.6</version>
</dependency>
<!-- Bean Validation API support -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,30 @@
package org.openapitools;
import com.fasterxml.jackson.databind.Module;
import org.openapitools.jackson.nullable.JsonNullableModule;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.FullyQualifiedAnnotationBeanNameGenerator;
@SpringBootApplication(
nameGenerator = FullyQualifiedAnnotationBeanNameGenerator.class
)
@ComponentScan(
basePackages = {"org.openapitools", "org.openapitools.api" , "org.openapitools.configuration"},
nameGenerator = FullyQualifiedAnnotationBeanNameGenerator.class
)
public class OpenApiGeneratorApplication {
public static void main(String[] args) {
SpringApplication.run(OpenApiGeneratorApplication.class, args);
}
@Bean(name = "org.openapitools.OpenApiGeneratorApplication.jsonNullableModule")
public Module jsonNullableModule() {
return new JsonNullableModule();
}
}

View File

@@ -0,0 +1,38 @@
package org.openapitools;
import com.fasterxml.jackson.databind.util.StdDateFormat;
import java.text.DateFormat;
import java.text.FieldPosition;
import java.text.ParsePosition;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;
public class RFC3339DateFormat extends DateFormat {
private static final long serialVersionUID = 1L;
private static final TimeZone TIMEZONE_Z = TimeZone.getTimeZone("UTC");
private final StdDateFormat fmt = new StdDateFormat()
.withTimeZone(TIMEZONE_Z)
.withColonInTimeZone(true);
public RFC3339DateFormat() {
this.calendar = new GregorianCalendar();
}
@Override
public Date parse(String source, ParsePosition pos) {
return fmt.parse(source, pos);
}
@Override
public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition) {
return fmt.format(date, toAppendTo, fieldPosition);
}
@Override
public Object clone() {
return this;
}
}

View File

@@ -0,0 +1,19 @@
package org.openapitools.api;
import org.springframework.web.context.request.NativeWebRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
public class ApiUtil {
public static void setExampleResponse(NativeWebRequest req, String contentType, String example) {
try {
HttpServletResponse res = req.getNativeResponse(HttpServletResponse.class);
res.setCharacterEncoding("UTF-8");
res.addHeader("Content-Type", contentType);
res.getWriter().print(example);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,88 @@
/**
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) (7.13.0-SNAPSHOT).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
package org.openapitools.api;
import org.openapitools.model.Dog;
import org.openapitools.model.Error;
import io.swagger.v3.oas.annotations.ExternalDocumentation;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import jakarta.validation.Valid;
import jakarta.validation.constraints.*;
import java.util.List;
import java.util.Map;
import jakarta.annotation.Generated;
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", comments = "Generator version: 7.13.0-SNAPSHOT")
@Validated
@Tag(name = "dogs", description = "the dogs API")
public interface DogsApi {
default DogsApiDelegate getDelegate() {
return new DogsApiDelegate() {};
}
/**
* POST /dogs : Create a dog
*
* @param dog (optional)
* @return OK (status code 200)
* or Bad Request (status code 400)
*/
@Operation(
operationId = "createDog",
summary = "Create a dog",
responses = {
@ApiResponse(responseCode = "200", description = "OK", content = {
@Content(mediaType = "application/json", schema = @Schema(implementation = Dog.class))
}),
@ApiResponse(responseCode = "400", description = "Bad Request", content = {
@Content(mediaType = "application/json", schema = @Schema(implementation = Error.class), examples = {
@ExampleObject(
name = "DogNameBiggerThan50Error",
value = "{\"code\":400,\"message\":\"name size must be between 0 and 50\"}"
),
@ExampleObject(
name = "DogNameContainsNumbersError",
value = "{\"code\":400,\"message\":\"Name must contain only letters\"}"
),
@ExampleObject(
name = "DogAgeNegativeError",
value = "{\"code\":400,\"message\":\"age must be greater than or equal to 0\"}"
)
})
})
}
)
@RequestMapping(
method = RequestMethod.POST,
value = "/dogs",
produces = { "application/json" },
consumes = { "application/json" }
)
default ResponseEntity<Dog> createDog(
@Parameter(name = "Dog", description = "") @Valid @RequestBody(required = false) Dog dog
) {
return getDelegate().createDog(dog);
}
}

View File

@@ -0,0 +1,45 @@
package org.openapitools.api;
import org.openapitools.model.Dog;
import org.openapitools.model.Error;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.multipart.MultipartFile;
import jakarta.validation.constraints.*;
import jakarta.validation.Valid;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import jakarta.annotation.Generated;
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen")
@Controller
@RequestMapping("${openapi.noExamplesInAnnotationExample.base-path:}")
public class DogsApiController implements DogsApi {
private final DogsApiDelegate delegate;
public DogsApiController(@Autowired(required = false) DogsApiDelegate delegate) {
this.delegate = Optional.ofNullable(delegate).orElse(new DogsApiDelegate() {});
}
@Override
public DogsApiDelegate getDelegate() {
return delegate;
}
}

View File

@@ -0,0 +1,56 @@
package org.openapitools.api;
import org.openapitools.model.Dog;
import org.openapitools.model.Error;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.multipart.MultipartFile;
import jakarta.validation.constraints.*;
import jakarta.validation.Valid;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import jakarta.annotation.Generated;
/**
* A delegate to be called by the {@link DogsApiController}}.
* Implement this interface with a {@link org.springframework.stereotype.Service} annotated class.
*/
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", comments = "Generator version: 7.13.0-SNAPSHOT")
public interface DogsApiDelegate {
default Optional<NativeWebRequest> getRequest() {
return Optional.empty();
}
/**
* POST /dogs : Create a dog
*
* @param dog (optional)
* @return OK (status code 200)
* or Bad Request (status code 400)
* @see DogsApi#createDog
*/
default ResponseEntity<Dog> createDog(Dog dog) {
getRequest().ifPresent(request -> {
for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) {
if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) {
String exampleString = "{ \"name\" : \"Rex\", \"age\" : 5 }";
ApiUtil.setExampleResponse(request, "application/json", exampleString);
break;
}
if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) {
String exampleString = "{ \"code\" : 0, \"message\" : \"message\" }";
ApiUtil.setExampleResponse(request, "application/json", exampleString);
break;
}
}
});
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
}

View File

@@ -0,0 +1,20 @@
package org.openapitools.configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.GetMapping;
/**
* Home redirection to OpenAPI api documentation
*/
@Controller
public class HomeController {
@RequestMapping("/")
public String index() {
return "redirect:swagger-ui.html";
}
}

View File

@@ -0,0 +1,27 @@
package org.openapitools.configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.security.SecurityScheme;
@Configuration
public class SpringDocConfiguration {
@Bean(name = "org.openapitools.configuration.SpringDocConfiguration.apiInfo")
OpenAPI apiInfo() {
return new OpenAPI()
.info(
new Info()
.title("No examples in annotation example API")
.description("No examples in annotation example API")
.version("1.0.0")
)
;
}
}

View File

@@ -0,0 +1,109 @@
package org.openapitools.model;
import java.net.URI;
import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonCreator;
import org.springframework.lang.Nullable;
import org.openapitools.jackson.nullable.JsonNullable;
import java.time.OffsetDateTime;
import jakarta.validation.Valid;
import jakarta.validation.constraints.*;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.*;
import jakarta.annotation.Generated;
/**
* Dog
*/
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", comments = "Generator version: 7.13.0-SNAPSHOT")
public class Dog {
private @Nullable String name;
private @Nullable Integer age;
public Dog name(String name) {
this.name = name;
return this;
}
/**
* Get name
* @return name
*/
@Pattern(regexp = "^[a-zA-Z]+$", message="Name must contain only letters") @Size(max = 50)
@Schema(name = "name", example = "Rex", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
@JsonProperty("name")
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Dog age(Integer age) {
this.age = age;
return this;
}
/**
* Get age
* minimum: 0
* @return age
*/
@Min(0)
@Schema(name = "age", example = "5", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
@JsonProperty("age")
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Dog dog = (Dog) o;
return Objects.equals(this.name, dog.name) &&
Objects.equals(this.age, dog.age);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("class Dog {\n");
sb.append(" name: ").append(toIndentedString(name)).append("\n");
sb.append(" age: ").append(toIndentedString(age)).append("\n");
sb.append("}");
return sb.toString();
}
/**
* Convert the given object to string with each line indented by 4 spaces
* (except the first line).
*/
private String toIndentedString(Object o) {
if (o == null) {
return "null";
}
return o.toString().replace("\n", "\n ");
}
}

View File

@@ -0,0 +1,108 @@
package org.openapitools.model;
import java.net.URI;
import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonCreator;
import org.springframework.lang.Nullable;
import org.openapitools.jackson.nullable.JsonNullable;
import java.time.OffsetDateTime;
import jakarta.validation.Valid;
import jakarta.validation.constraints.*;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.*;
import jakarta.annotation.Generated;
/**
* Error
*/
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", comments = "Generator version: 7.13.0-SNAPSHOT")
public class Error {
private @Nullable Integer code;
private @Nullable String message;
public Error code(Integer code) {
this.code = code;
return this;
}
/**
* Get code
* @return code
*/
@Schema(name = "code", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
@JsonProperty("code")
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public Error message(String message) {
this.message = message;
return this;
}
/**
* Get message
* @return message
*/
@Schema(name = "message", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
@JsonProperty("message")
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Error error = (Error) o;
return Objects.equals(this.code, error.code) &&
Objects.equals(this.message, error.message);
}
@Override
public int hashCode() {
return Objects.hash(code, message);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("class Error {\n");
sb.append(" code: ").append(toIndentedString(code)).append("\n");
sb.append(" message: ").append(toIndentedString(message)).append("\n");
sb.append("}");
return sb.toString();
}
/**
* Convert the given object to string with each line indented by 4 spaces
* (except the first line).
*/
private String toIndentedString(Object o) {
if (o == null) {
return "null";
}
return o.toString().replace("\n", "\n ");
}
}

View File

@@ -0,0 +1,3 @@
server.port=8080
spring.jackson.date-format=org.openapitools.RFC3339DateFormat
spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS=false

View File

@@ -0,0 +1,83 @@
openapi: 3.0.3
info:
description: No examples in annotation example API
title: No examples in annotation example API
version: 1.0.0
servers:
- url: https://localhost:8080
paths:
/dogs:
post:
operationId: createDog
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Dog'
responses:
"200":
content:
application/json:
schema:
$ref: '#/components/schemas/Dog'
description: OK
"400":
content:
application/json:
examples:
dog name length:
$ref: '#/components/examples/DogNameBiggerThan50Error'
dog name contains numbers:
$ref: '#/components/examples/DogNameContainsNumbersError'
dog age negative:
$ref: '#/components/examples/DogAgeNegativeError'
schema:
$ref: '#/components/schemas/Error'
description: Bad Request
summary: Create a dog
x-content-type: application/json
x-accepts:
- application/json
components:
examples:
DogNameBiggerThan50Error:
value:
code: 400
message: name size must be between 0 and 50
DogNameContainsNumbersError:
value:
code: 400
message: Name must contain only letters
DogAgeNegativeError:
value:
code: 400
message: age must be greater than or equal to 0
schemas:
Dog:
example:
name: Rex
age: 5
properties:
name:
example: Rex
maxLength: 50
pattern: "^[a-zA-Z]+$"
type: string
x-pattern-message: Name must contain only letters
age:
example: 5
format: int32
minimum: 0
type: integer
type: object
Error:
example:
code: 0
message: message
properties:
code:
format: int32
type: integer
message:
type: string
type: object

View File

@@ -0,0 +1,13 @@
package org.openapitools;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class OpenApiGeneratorApplicationTests {
@Test
void contextLoads() {
}
}