diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/api/AbstractOpenApiResource.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/api/AbstractOpenApiResource.java
index 9b6d5ffa5..45c7163b0 100644
--- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/api/AbstractOpenApiResource.java
+++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/api/AbstractOpenApiResource.java
@@ -469,6 +469,7 @@ private static void handleComponentSchemaTypes(OpenAPI openAPI) {
}
for (Schema> schema : openAPI.getComponents().getSchemas().values()) {
SpringDocUtils.fixNullOnlyAdditionalProperties(schema);
+ SpringDocUtils.fixNullMutatedObjectSchema(schema);
}
}
diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/utils/SpringDocUtils.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/utils/SpringDocUtils.java
index e705d1b09..10ef0f416 100644
--- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/utils/SpringDocUtils.java
+++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/utils/SpringDocUtils.java
@@ -215,6 +215,36 @@ else if (types == null && "null".equals(addPropSchema.getType())) {
}
}
+ /**
+ * Fix component schemas that have been incorrectly mutated to {@code type: "null"} when they
+ * have properties, which indicates they should be {@code type: "object"}.
+ *
+ *
This can happen when {@code @Nullable} is applied to a field whose type is represented
+ * as a {@code $ref} component schema. swagger-core may propagate the nullable marker onto
+ * the shared component schema itself, corrupting it for all references. This method restores
+ * such schemas to {@code type: "object"} based on the presence of {@code properties}.
+ *
+ * @param schema the schema to fix
+ * @see Issue #3275
+ */
+ public static void fixNullMutatedObjectSchema(Schema> schema) {
+ if (schema == null || schema.getProperties() == null || schema.getProperties().isEmpty()) {
+ return;
+ }
+ Set types = schema.getTypes();
+ boolean isNullOnly = (types != null && types.size() == 1 && types.contains("null"))
+ || (types == null && "null".equals(schema.getType()));
+ if (isNullOnly) {
+ if (types != null) {
+ types.remove("null");
+ types.add("object");
+ }
+ else {
+ schema.setType("object");
+ }
+ }
+ }
+
/**
* Handle schema types.
*
diff --git a/springdoc-openapi-tests/pom.xml b/springdoc-openapi-tests/pom.xml
index f66c71499..df7f4e9fe 100644
--- a/springdoc-openapi-tests/pom.xml
+++ b/springdoc-openapi-tests/pom.xml
@@ -2,7 +2,7 @@
springdoc-openapi
org.springdoc
- 2.8.17-SNAPSHOT
+ 2.8.18-SNAPSHOT
pom
4.0.0
diff --git a/springdoc-openapi-tests/springdoc-openapi-actuator-webflux-tests/pom.xml b/springdoc-openapi-tests/springdoc-openapi-actuator-webflux-tests/pom.xml
index 1133fc3d0..aacb3825f 100644
--- a/springdoc-openapi-tests/springdoc-openapi-actuator-webflux-tests/pom.xml
+++ b/springdoc-openapi-tests/springdoc-openapi-actuator-webflux-tests/pom.xml
@@ -2,7 +2,7 @@
springdoc-openapi-tests
org.springdoc
- 2.8.17-SNAPSHOT
+ 2.8.18-SNAPSHOT
4.0.0
diff --git a/springdoc-openapi-tests/springdoc-openapi-actuator-webmvc-tests/pom.xml b/springdoc-openapi-tests/springdoc-openapi-actuator-webmvc-tests/pom.xml
index 0a06452e9..95d18e53f 100644
--- a/springdoc-openapi-tests/springdoc-openapi-actuator-webmvc-tests/pom.xml
+++ b/springdoc-openapi-tests/springdoc-openapi-actuator-webmvc-tests/pom.xml
@@ -2,7 +2,7 @@
springdoc-openapi-tests
org.springdoc
- 2.8.17-SNAPSHOT
+ 2.8.18-SNAPSHOT
4.0.0
diff --git a/springdoc-openapi-tests/springdoc-openapi-data-rest-tests/pom.xml b/springdoc-openapi-tests/springdoc-openapi-data-rest-tests/pom.xml
index 531c8a2ca..a31f0ac97 100644
--- a/springdoc-openapi-tests/springdoc-openapi-data-rest-tests/pom.xml
+++ b/springdoc-openapi-tests/springdoc-openapi-data-rest-tests/pom.xml
@@ -2,7 +2,7 @@
springdoc-openapi-tests
org.springdoc
- 2.8.17-SNAPSHOT
+ 2.8.18-SNAPSHOT
4.0.0
springdoc-openapi-data-rest-tests
diff --git a/springdoc-openapi-tests/springdoc-openapi-function-webflux-tests/pom.xml b/springdoc-openapi-tests/springdoc-openapi-function-webflux-tests/pom.xml
index 3eec491a9..93479ab01 100644
--- a/springdoc-openapi-tests/springdoc-openapi-function-webflux-tests/pom.xml
+++ b/springdoc-openapi-tests/springdoc-openapi-function-webflux-tests/pom.xml
@@ -2,7 +2,7 @@
springdoc-openapi-tests
org.springdoc
- 2.8.17-SNAPSHOT
+ 2.8.18-SNAPSHOT
4.0.0
diff --git a/springdoc-openapi-tests/springdoc-openapi-function-webmvc-tests/pom.xml b/springdoc-openapi-tests/springdoc-openapi-function-webmvc-tests/pom.xml
index 75c04e299..1a22650e8 100644
--- a/springdoc-openapi-tests/springdoc-openapi-function-webmvc-tests/pom.xml
+++ b/springdoc-openapi-tests/springdoc-openapi-function-webmvc-tests/pom.xml
@@ -2,7 +2,7 @@
springdoc-openapi-tests
org.springdoc
- 2.8.17-SNAPSHOT
+ 2.8.18-SNAPSHOT
4.0.0
diff --git a/springdoc-openapi-tests/springdoc-openapi-groovy-tests/pom.xml b/springdoc-openapi-tests/springdoc-openapi-groovy-tests/pom.xml
index 303752232..f15ba3a31 100644
--- a/springdoc-openapi-tests/springdoc-openapi-groovy-tests/pom.xml
+++ b/springdoc-openapi-tests/springdoc-openapi-groovy-tests/pom.xml
@@ -3,7 +3,7 @@
org.springdoc
springdoc-openapi-tests
- 2.8.17-SNAPSHOT
+ 2.8.18-SNAPSHOT
springdoc-openapi-groovy-tests
${project.artifactId}
diff --git a/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/pom.xml b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/pom.xml
index ef2ceff5f..1a98d43b3 100644
--- a/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/pom.xml
+++ b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/pom.xml
@@ -2,7 +2,7 @@
springdoc-openapi-tests
org.springdoc
- 2.8.17-SNAPSHOT
+ 2.8.18-SNAPSHOT
4.0.0
springdoc-openapi-hateoas-tests
diff --git a/springdoc-openapi-tests/springdoc-openapi-javadoc-tests/pom.xml b/springdoc-openapi-tests/springdoc-openapi-javadoc-tests/pom.xml
index 66b2a5cf1..e7be050d6 100644
--- a/springdoc-openapi-tests/springdoc-openapi-javadoc-tests/pom.xml
+++ b/springdoc-openapi-tests/springdoc-openapi-javadoc-tests/pom.xml
@@ -2,7 +2,7 @@
org.springdoc
springdoc-openapi-tests
- 2.8.17-SNAPSHOT
+ 2.8.18-SNAPSHOT
4.0.0
diff --git a/springdoc-openapi-tests/springdoc-openapi-javadoc-tests/src/test/java/test/org/springdoc/api/v31/app175/Inner.java b/springdoc-openapi-tests/springdoc-openapi-javadoc-tests/src/test/java/test/org/springdoc/api/v31/app175/Inner.java
new file mode 100644
index 000000000..75cb67a1f
--- /dev/null
+++ b/springdoc-openapi-tests/springdoc-openapi-javadoc-tests/src/test/java/test/org/springdoc/api/v31/app175/Inner.java
@@ -0,0 +1,37 @@
+/*
+ *
+ * *
+ * * *
+ * * * *
+ * * * * *
+ * * * * * * Copyright 2019-2026 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.v31.app175;
+
+/**
+ * Inner record used as a referenced component schema.
+ *
+ * @param lines the line count
+ * @param bytes the byte count
+ * @param bucket the bucket name
+ * @param key the object key
+ */
+public record Inner(int lines, long bytes, String bucket, String key) {}
\ No newline at end of file
diff --git a/springdoc-openapi-tests/springdoc-openapi-javadoc-tests/src/test/java/test/org/springdoc/api/v31/app175/NullableRefController.java b/springdoc-openapi-tests/springdoc-openapi-javadoc-tests/src/test/java/test/org/springdoc/api/v31/app175/NullableRefController.java
new file mode 100644
index 000000000..5e8c6e55b
--- /dev/null
+++ b/springdoc-openapi-tests/springdoc-openapi-javadoc-tests/src/test/java/test/org/springdoc/api/v31/app175/NullableRefController.java
@@ -0,0 +1,49 @@
+/*
+ *
+ * *
+ * * *
+ * * * *
+ * * * * *
+ * * * * * * Copyright 2019-2026 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.v31.app175;
+
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * Controller for regression test of issue #3275.
+ * Verifies that a {@code @Nullable} field referencing a component schema does not
+ * mutate the referenced schema's type to {@code "null"}.
+ */
+@RestController
+class NullableRefController {
+
+ /**
+ * Returns an Outer record with a nullable Inner reference.
+ *
+ * @return the outer record
+ */
+ @GetMapping("/outer")
+ public Outer getOuter() {
+ return new Outer("x", null);
+ }
+}
\ No newline at end of file
diff --git a/springdoc-openapi-tests/springdoc-openapi-javadoc-tests/src/test/java/test/org/springdoc/api/v31/app175/Outer.java b/springdoc-openapi-tests/springdoc-openapi-javadoc-tests/src/test/java/test/org/springdoc/api/v31/app175/Outer.java
new file mode 100644
index 000000000..f9ceed91a
--- /dev/null
+++ b/springdoc-openapi-tests/springdoc-openapi-javadoc-tests/src/test/java/test/org/springdoc/api/v31/app175/Outer.java
@@ -0,0 +1,37 @@
+/*
+ *
+ * *
+ * * *
+ * * * *
+ * * * * *
+ * * * * * * Copyright 2019-2026 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.v31.app175;
+
+import org.springframework.lang.Nullable;
+
+/**
+ * Outer record with a nullable reference to {@link Inner}.
+ *
+ * @param id the identifier
+ * @param result the optional inner result, may be null
+ */
+public record Outer(String id, @Nullable Inner result) {}
\ No newline at end of file
diff --git a/springdoc-openapi-tests/springdoc-openapi-javadoc-tests/src/test/java/test/org/springdoc/api/v31/app175/SpringDocApp175Test.java b/springdoc-openapi-tests/springdoc-openapi-javadoc-tests/src/test/java/test/org/springdoc/api/v31/app175/SpringDocApp175Test.java
new file mode 100644
index 000000000..2479c0a54
--- /dev/null
+++ b/springdoc-openapi-tests/springdoc-openapi-javadoc-tests/src/test/java/test/org/springdoc/api/v31/app175/SpringDocApp175Test.java
@@ -0,0 +1,46 @@
+/*
+ *
+ * *
+ * * *
+ * * * *
+ * * * * *
+ * * * * * * Copyright 2019-2026 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.v31.app175;
+
+import test.org.springdoc.api.v31.AbstractSpringDocTest;
+
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ * Test for issue #3275: verifies that a {@code @Nullable} field referencing a component schema
+ * does not mutate the referenced schema's type to {@code "null"}.
+ */
+class SpringDocApp175Test extends AbstractSpringDocTest {
+
+ /**
+ * The type Spring doc test app.
+ */
+ @SpringBootApplication
+ static class SpringDocTestApp {
+ }
+
+}
\ No newline at end of file
diff --git a/springdoc-openapi-tests/springdoc-openapi-javadoc-tests/src/test/resources/results/3.1.0/app175.json b/springdoc-openapi-tests/springdoc-openapi-javadoc-tests/src/test/resources/results/3.1.0/app175.json
new file mode 100644
index 000000000..d544d08c2
--- /dev/null
+++ b/springdoc-openapi-tests/springdoc-openapi-javadoc-tests/src/test/resources/results/3.1.0/app175.json
@@ -0,0 +1,69 @@
+{
+ "openapi": "3.1.0",
+ "info": {
+ "title": "OpenAPI definition",
+ "version": "v0"
+ },
+ "servers": [
+ {
+ "url": "http://localhost",
+ "description": "Generated server url"
+ }
+ ],
+ "paths": {
+ "/outer": {
+ "get": {
+ "tags": [
+ "nullable-ref-controller"
+ ],
+ "operationId": "getOuter",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "*/*": {
+ "schema": {
+ "$ref": "#/components/schemas/Outer"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "Inner": {
+ "type": "object",
+ "properties": {
+ "lines": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "bytes": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "bucket": {
+ "type": "string"
+ },
+ "key": {
+ "type": "string"
+ }
+ }
+ },
+ "Outer": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "result": {
+ "$ref": "#/components/schemas/Inner"
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/springdoc-openapi-tests/springdoc-openapi-kotlin-webflux-tests/pom.xml b/springdoc-openapi-tests/springdoc-openapi-kotlin-webflux-tests/pom.xml
index e5c8e955c..7f09cf189 100644
--- a/springdoc-openapi-tests/springdoc-openapi-kotlin-webflux-tests/pom.xml
+++ b/springdoc-openapi-tests/springdoc-openapi-kotlin-webflux-tests/pom.xml
@@ -2,7 +2,7 @@
springdoc-openapi-tests
org.springdoc
- 2.8.17-SNAPSHOT
+ 2.8.18-SNAPSHOT
4.0.0
springdoc-openapi-kotlin-webflux-tests
diff --git a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/pom.xml b/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/pom.xml
index 0da030bfe..c4028baa6 100644
--- a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/pom.xml
+++ b/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/pom.xml
@@ -2,7 +2,7 @@
springdoc-openapi-tests
org.springdoc
- 2.8.17-SNAPSHOT
+ 2.8.18-SNAPSHOT
4.0.0
springdoc-openapi-kotlin-webmvc-tests
diff --git a/springdoc-openapi-tests/springdoc-openapi-security-tests/pom.xml b/springdoc-openapi-tests/springdoc-openapi-security-tests/pom.xml
index 8418dcace..bf84ee17c 100644
--- a/springdoc-openapi-tests/springdoc-openapi-security-tests/pom.xml
+++ b/springdoc-openapi-tests/springdoc-openapi-security-tests/pom.xml
@@ -3,7 +3,7 @@
org.springdoc
springdoc-openapi-tests
- 2.8.17-SNAPSHOT
+ 2.8.18-SNAPSHOT
springdoc-openapi-security-tests
${project.artifactId}