Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Swagger adds extra line breaks in yaml generated from annotated code #4846

Open
igiguere opened this issue Feb 21, 2025 · 0 comments
Open

Swagger adds extra line breaks in yaml generated from annotated code #4846

igiguere opened this issue Feb 21, 2025 · 0 comments

Comments

@igiguere
Copy link

igiguere commented Feb 21, 2025

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 ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant