swagger-api / swagger-api/swagger-core

Swagger adds extra line breaks in yaml generated from annotated code

Open
#4,846 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

backlog
Dominant language
Java
Stars
7.5k
Forks
2.3k
Avg merge
18h 1m
Merged PRs (30d)
10

Description

Summary

I'm using annotations in a Spring Boot project to generate the OpenAPI documentation.

In the resulting yaml, some descriptions and examples are written as strings, wrapped in quotes, and with escaped line breaks that do not appear in the annotated code. Other examples and descriptions appear as yaml "data" ('|' = new line): they are not wrapped in quotes, and the added line breaks are not escaped.

All the examples are json strings, but one is re-interpreted as yaml properties and aligned with the rest of the yaml document.

The OpenApi document is generated at runtime.

Note that I used both PostMan and Curl to obtain the yaml document. Both tools produce the same output. Therefore I doubt this behavior could be a limitation of either tool.

Environment

mvn -version
Apache Maven 3.9.6 (bc0240f3c744dd6b6ec2920b3cd08dcc295161ae)
Maven home: /opt/build-tools/maven/apache-maven-3.9.6
Java version: 17.0.13, vendor: Red Hat, Inc., runtime: /usr/lib/jvm/java-17-openjdk-17.0.13.0.11-4.0.1.el9.x86_64
Default locale: en_US, platform encoding: UTF-8
OS name: "linux", version: "5.15.0-302.167.6.1.el9uek.x86_64", arch: "amd64", family: "unix"

Spring Boot version : 3.4.0
Spring Framework version : 6.2.0
Jakarta annotations API version : 3.0.0

Swagger and OpenApi dependencies:

  • org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0
  • io.swagger.core.v3:swagger-models-jakarta:2.2.22
  • io.swagger.core.v3:swagger-annotations-jakarta:2.2.22
  • io.swagger.core.v3:swagger-core-jakarta:2.2.22
Code and Output
Tags

Tags annotated in code. Each tag is annotated on a different method of one class, each method serves a REST endpoint.

    @Tag(name = "Version", description = "Version of the application.")
    @Tag(name = "Classify",
            description = "Classify files using ML trained models and benefit from multi-domain classifications for both the texts and images.")
    @Tag(name = "Detect", description = "Detect document specifics such as language and protection.")
    @Tag(name = "Extract",
            description = "Extract to get textual content from text, image, video files and expose editorial metadata from file properties.")
    @Tag(name = "Process",
            description = "Process to identify and assign risks to critical data points such as PII, PSI and PHI using hybrid AI models that focus on semantic metadata analysis.")
    @Tag(name = "Compose",
            description = "Compose to build and apply action chains regrouping extract, detect, process and classify tasks.")

Tags generated in yaml:
Notice the "Classify" description is yaml data, with an unescaped line break. The other longer descriptions are strings, with escaped line breaks.

tags:
- name: Extract
  description: "Extract to get textual content from text, image, video files and expose\
    \ editorial metadata from file properties."
- name: Process
  description: "Process to identify and assign risks to critical data points such\
    \ as PII, PSI and PHI using hybrid AI models that focus on semantic metadata analysis."
- name: Detect
  description: Detect document specifics such as language and protection.
- name: Version
  description: Version of the application.
- name: Compose
  description: "Compose to build and apply action chains regrouping extract, detect,\
    \ process and classify tasks."
- name: Classify
  description: Classify files using ML trained models and benefit from multi-domain
    classifications for both the texts and images.
Examples

These are the examples annotated in code. Each example is annotated in an @ApiResponse of a different method of one class.
The strings for the examples are provided by text block constants, to preserve the json strings.

Constant:

    private static final String CLASSIFY_RESPONSE_EXAMPLE = """
            {"status":{"status":"SUCCESS","action":"risk-classification","documentName":"file.pdf"},"riskClassification":{"header":{"documentName":"file.pdf","documentRiskStatus":"high"},"result":{"pii":[{"riskName":"BankAccount","riskLevel":"high","frequency":2}]}}}
            """;
    private static final String DETECT_RESPONSE_EXAMPLE = """
            {"status":{"status":"SUCCESS","action":"risk-detection","documentName":"file.pdf"},"riskDetection":{"header":{"documentId":"","status":{"message":"SUCCESS","description":"Processing succeeded."}},"results":{"protection":{"status":{"message":"SUCCESS","description":"Processing succeeded."},"result":{"isProtected":false}}}}}
            """;
    private static final String EXTRACT_RESPONSE_EXAMPLE = """
            {"status":{"status":"SUCCESS","action":"risk-extraction","documentName":"image.PNG"},"riskExtraction":{"header":{"documentId":"","status":{"message":"SUCCESS","description":"Processing succeeded."}},"results":{"idol-ocr":{"status":{"message":"SUCCESS","description":"Processing succeeded."},"result":{"results":"text","status":"SUCCESS"}}}}}
            """;
    private static final String COMPOSE_RESPONSE_EXAMPLE = """
            {"status":{"status":"SUCCESS","action":"risk-composition","documentName":"file.pdf"},"riskComposition":{"header":{"documentId":"","status":{"message":"SUCCESS","description":"Processing succeeded."}},"results":{"protection":{"status":{"message":"SUCCESS","description":"Processing succeeded."},"result":{"isProtected":false}}}}}
            """;
    private static final String PROCESS_RESPONSE_EXAMPLE = """
            {"header":{"documentId":"","status":{"message":"SUCCESS","description":"Processing succeeded."}},"results":{"tme":{"status":{"message":"UNSUPPORTED","description":"Undetectable language."},"result":null}}
            """;
    private static final String VERSION_RESPONSE_EXAMPLE = """
            {"projectVersion":"24.2.0.0","buildNumber":"1169"}
            """;

ApiResponse annotations:

@ApiResponse(description = "Successful operation", responseCode = "200",  content = @Content(mediaType = "application/json",
     schema = @Schema(implementation = ProductVersion.class), examples = @ExampleObject(value = VERSION_RESPONSE_EXAMPLE)))
@ApiResponse(description = "Successful operation", responseCode = "200", content = @Content(mediaType = "application/json",
    schema = @Schema(implementation = ResponseEntity.class, description = "full response."), examples = @ExampleObject(value = CLASSIFY_RESPONSE_EXAMPLE)))
@ApiResponse(description = "Successful operation", responseCode = "200", content = @Content(mediaType = "application/json",
    schema = @Schema(implementation = ResponseEntity.class, description = "full response."), examples = @ExampleObject(value = DETECT_RESPONSE_EXAMPLE)))
@ApiResponse(description = "Successful operation", responseCode = "200", content = @Content(mediaType = "application/json",
    schema = @Schema(implementation = ResponseEntity.class, description = "full response."), examples = @ExampleObject(value = EXTRACT_RESPONSE_EXAMPLE)))
@ApiResponse(description = "Successful operation", responseCode = "200", content = @Content(mediaType = "application/json",
    schema = @Schema(implementation = ResponseEntity.class, description = "full response."), examples = @ExampleObject(value = PROCESS_RESPONSE_EXAMPLE)))
@ApiResponse(description = "Successful operation", responseCode = "200", content = @Content(mediaType = "application/json",
    schema = @Schema(implementation = ResponseEntity.class, description = "full response."), examples = @ExampleObject(value = COMPOSE_RESPONSE_EXAMPLE)))

Examples generated in yaml:

From VERSION_RESPONSE_EXAMPLE: yaml properties!

              example:
                projectVersion: 24.2.0.0
                buildNumber: "1169"

From CLASSIFY_RESPONSE_EXAMPLE: quoted string with escaped line breaks

              example: "{\"status\":{\"status\":\"SUCCESS\",\"action\":\"risk-classification\"\
                ,\"documentName\":\"file.pdf\"},\"riskClassification\":{\"header\"\
                :{\"documentName\":\"file.pdf\",\"documentRiskStatus\":\"high\"},\"\
                result\":{\"pii\":[{\"riskName\":\"BankAccount\",\"riskLevel\":\"\
                high\",\"frequency\":2}]}}}"

From DETECT_RESPONSE_EXAMPLE: quoted string with escaped line breaks

              example: "{\"status\":{\"status\":\"SUCCESS\",\"action\":\"risk-detection\"\
                ,\"documentName\":\"file.pdf\"},\"riskDetection\":{\"header\":{\"\
                documentId\":\"\",\"status\":{\"message\":\"SUCCESS\",\"description\"\
                :\"Processing succeeded.\"}},\"results\":{\"protection\":{\"status\"\
                :{\"message\":\"SUCCESS\",\"description\":\"Processing succeeded.\"\
                },\"result\":{\"isProtected\":false}}}}}"

From EXTRACT_RESPONSE_EXAMPLE: quoted string with escaped line breaks

              example: "{\"status\":{\"status\":\"SUCCESS\",\"action\":\"risk-extraction\"\
                ,\"documentName\":\"image.PNG\"},\"riskExtraction\":{\"header\":{\"\
                documentId\":\"\",\"status\":{\"message\":\"SUCCESS\",\"description\"\
                :\"Processing succeeded.\"}},\"results\":{\"idol-ocr\":{\"status\"\
                :{\"message\":\"SUCCESS\",\"description\":\"Processing succeeded.\"\
                },\"result\":{\"results\":\"text\",\"status\":\"SUCCESS\"}}}}}"

From PROCESS_RESPONSE_EXAMPLE: yaml data on a single line

              example: |
                {"header":{"documentId":"","status":{"message":"SUCCESS","description":"Processing succeeded."}},"results":{"tme":{"status":{"message":"UNSUPPORTED","description":"Undetectable language."},"result":null}}

From COMPOSE_RESPONSE_EXAMPLE: quoted string with escaped line breaks

              example: "{\"status\":{\"status\":\"SUCCESS\",\"action\":\"risk-composition\"\
                ,\"documentName\":\"file.pdf\"},\"riskComposition\":{\"header\":{\"\
                documentId\":\"\",\"status\":{\"message\":\"SUCCESS\",\"description\"\
                :\"Processing succeeded.\"}},\"results\":{\"protection\":{\"status\"\
                :{\"message\":\"SUCCESS\",\"description\":\"Processing succeeded.\"\
                },\"result\":{\"isProtected\":false}}}}}"
Expected Behavior

Swagger should not add extra line breaks where there are none. The full-length strings from the annotated code should be rendered without line breaks, escaped or not.
The strings from annotated code should always be interpreted in the same way: either always as quoted strings or as yaml data ('|' = next line), but not toggle from one representation to the other.

The strings from annotated code should not be re-interpreted as yaml properties and embedded in the yaml document.

Question

It is possible, when starting from the OpenApi document (json or yaml), to define minimum and maximum string length: https://swagger.io/docs/specification/v3_0/data-models/data-types/

Is there an equivalent setting that can be used when generating the OpenApi document from code annotations ? Properties to be set in Spring Boot application.properties, perhaps ?

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Reproduce runtime OpenAPI generation from the annotated Spring Boot examples, retrieving the YAML with Postman or Curl. Compare descriptions and JSON examples from the annotations with the generated YAML, focusing on inconsistent quoting, escaped line breaks, and reinterpreted properties. Done means equivalent strings are emitted consistently without added line breaks or YAML reinterpretation.

Written by the indexing model from the issue text.

Assessment

Tech stack
java, spring-boot
Domain
api
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.