Repository: springdoc/springdoc-openapi
Pull request #2561 (2024-04-04 02:02:32)

NPE occurs when outputting an OpenAPI document since 2.5.0
NPE occurs when outputting an OpenAPI document in a project using springdoc-openapi 2.5.0.
It appears to occur only when certain annotations are included.
Specifically controller class with `@Header` annotation include `@Schema` attribute set.
```java
    @Operation(
        // .....
        responses = {
            @ApiResponse(
                responseCode = "201",
                description = "201 (Created)",
                headers = {
                    @Header(
                        name = HttpHeaders.LOCATION,
                        description = "Sample endpoint",
                        schema = @Schema(implementation = URI.class) // include this.
                    )
                })
        }
        // ....
    )
    @GetMapping
    public String foo(){....}
```

**To Reproduce**
Steps to reproduce the behavior:
1. clone https://github.com/footaku/minimum-springdoc-npe
1. ./gradlew bootRun
1. curl -v "http://localhost:8080/v3/api-docs"

- What version of spring-boot you are using?
    - Spring Boot 3.2.4
- What modules and versions of springdoc-openapi are you using?
    - springdoc-openapi 2.5.0
- What is the actual and the expected result using OpenAPI Description (yml or json)?
    - JSON
- Provide with a sample code (HelloController) or Test that reproduces the problem
    - minimun sample: https://github.com/footaku/minimum-springdoc-npe

**Expected behavior**
NPE does not happen, can get JSON response.

**Additional context**
Swagger-core have been added `getHeader` overload methods into `AnnotationsUtils`.
This change seems to have been made between `2.2.20` and `2.2.21`.
https://github.com/swagger-api/swagger-core/compare/v2.2.20...v2.2.21

Around here that is added.
https://github.com/swagger-api/swagger-core/blame/d5c37284f5dbb9b6e65858c25d55c1aa2969f8a2/modules/swagger-core/src/main/java/io/swagger/v3/core/util/AnnotationsUtils.java#L1290-L1326

If we do not pass components, it will be thrown NPE here.
https://github.com/swagger-api/swagger-core/blob/d5c37284f5dbb9b6e65858c25d55c1aa2969f8a2/modules/swagger-core/src/main/java/io/swagger/v3/core/util/AnnotationsUtils.java#L1387



Patch:
diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/OperationService.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/OperationService.java
index e673e6b1c..5a539b6b8 100644
--- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/OperationService.java
+++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/OperationService.java
@@ -3,7 +3,7 @@
  *  *
  *  *  *
  *  *  *  *
- *  *  *  *  * Copyright 2019-2022 the original author or authors.
+ *  *  *  *  * Copyright 2019-2024 the original author or authors.
  *  *  *  *  *
  *  *  *  *  * Licensed under the Apache License, Version 2.0 (the "License");
  *  *  *  *  * you may not use this file except in compliance with the License.
@@ -412,7 +412,7 @@ public class OperationService {
 
 			buildResponseContent(methodAttributes, components, classProduces, methodProduces, apiResponsesOp, response, apiResponseObject);
 
-			AnnotationsUtils.getHeaders(response.headers(), null, propertyResolverUtils.isOpenapi31()).ifPresent(apiResponseObject::headers);
+			AnnotationsUtils.getHeaders(response.headers(), components, null, propertyResolverUtils.isOpenapi31()).ifPresent(apiResponseObject::headers);
 			// Make schema as string if empty
 			calculateHeader(apiResponseObject);
 			if (isResponseObject(apiResponseObject)) {


Test patch:
diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app218/HelloController.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app218/HelloController.java
new file mode 100644
index 000000000..6749896a4
--- /dev/null
+++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app218/HelloController.java
@@ -0,0 +1,63 @@
+/*
+ *
+ *  *
+ *  *  *
+ *  *  *  * Copyright 2019-2024 the original author or authors.
+ *  *  *  *
+ *  *  *  * Licensed under the Apache License, Version 2.0 (the "License");
+ *  *  *  * you may not use this file except in compliance with the License.
+ *  *  *  * You may obtain a copy of the License at
+ *  *  *  *
+ *  *  *  *      https://www.apache.org/licenses/LICENSE-2.0
+ *  *  *  *
+ *  *  *  * Unless required by applicable law or agreed to in writing, software
+ *  *  *  * distributed under the License is distributed on an "AS IS" BASIS,
+ *  *  *  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  *  *  * See the License for the specific language governing permissions and
+ *  *  *  * limitations under the License.
+ *  *  *
+ *  *
+ *
+ */
+
+package test.org.springdoc.api.v30.app218;
+
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.headers.Header;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import org.springframework.http.HttpHeaders;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.net.URI;
+
+
+@RestController
+@RequestMapping("/")
+public class HelloController {
+
+    @Operation(
+        summary = "Summary",
+        description = "This is description.",
+        tags = {"Sample"},
+        responses = {
+            @ApiResponse(
+                responseCode = "201",
+                description = "201 (Created)",
+                headers =
+                @Header(
+                    name = HttpHeaders.LOCATION,
+                    description = "Sample endpoint",
+                    schema = @Schema(implementation = URI.class)
+                )
+            )
+        }
+    )
+    @GetMapping
+    public String get() {
+        return "Hello World!";
+    }
+}
diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app218/SpringDocApp218Test.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app218/SpringDocApp218Test.java
new file mode 100644
index 000000000..24b9d92b4
--- /dev/null
+++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app218/SpringDocApp218Test.java
@@ -0,0 +1,36 @@
+/*
+ *
+ *  * Copyright 2019-2024 the original author or authors.
+ *  *
+ *  * Licensed under the Apache License, Version 2.0 (the "License");
+ *  * you may not use this file except in compliance with the License.
+ *  * You may obtain a copy of the License at
+ *  *
+ *  *      https://www.apache.org/licenses/LICENSE-2.0
+ *  *
+ *  * Unless required by applicable law or agreed to in writing, software
+ *  * distributed under the License is distributed on an "AS IS" BASIS,
+ *  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  * See the License for the specific language governing permissions and
+ *  * limitations under the License.
+ *
+ */
+
+package test.org.springdoc.api.v30.app218;
+
+import org.springdoc.core.customizers.SpecPropertiesCustomizer;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.test.context.SpringBootTest;
+import test.org.springdoc.api.v30.AbstractSpringDocV30Test;
+
+/**
+ * <p>
+ * A test for {@link SpecPropertiesCustomizer}
+ */
+@SpringBootTest
+public class SpringDocApp218Test extends AbstractSpringDocV30Test {
+
+	@SpringBootApplication
+	static class SpringDocTestApp {}
+
+}
diff --git a/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app218.json b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app218.json
new file mode 100644
index 000000000..4aeec31eb
--- /dev/null
+++ b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app218.json
@@ -0,0 +1,48 @@
+{
+  "openapi": "3.0.1",
+  "info": {
+    "title": "OpenAPI definition",
+    "version": "v0"
+  },
+  "servers": [
+    {
+      "url": "http://localhost",
+      "description": "Generated server url"
+    }
+  ],
+  "paths": {
+    "/": {
+      "get": {
+        "tags": [
+          "Sample"
+        ],
+        "summary": "Summary",
+        "description": "This is description.",
+        "operationId": "get",
+        "responses": {
+          "201": {
+            "description": "201 (Created)",
+            "headers": {
+              "Location": {
+                "description": "Sample endpoint",
+                "style": "simple",
+                "schema": {
+                  "type": "string",
+                  "format": "uri"
+                }
+              }
+            },
+            "content": {
+              "*/*": {
+                "schema": {
+                  "type": "string"
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  },
+  "components": {}
+}