diff --git a/modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/media/ExampleObject.java b/modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/media/ExampleObject.java
index 9fa9088782..6fc74748b3 100644
--- a/modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/media/ExampleObject.java
+++ b/modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/media/ExampleObject.java
@@ -69,4 +69,22 @@
**/
String description() default "";
+ /**
+ * A classpath or file-system path from which to load the example value at annotation
+ * processing time. The loaded content is used as if specified inline via the {@link #value()} field.
+ * When both {@code externalFile} and {@code value} are set, {@code externalFile} takes
+ * precedence if the file is found; otherwise falls back to {@code value}.
+ *
+ *
Supported path formats:
+ *
+ * - {@code classpath:openapi/examples/user.json} — loads from classpath (default when no prefix)
+ * - {@code file:/absolute/path/to/example.json} — loads from filesystem
+ * - {@code openapi/examples/user.json} — no prefix, treated as classpath
+ *
+ *
+ * @since 2.2.51
+ * @return path to a file containing the example value
+ **/
+ String externalFile() default "";
+
}
diff --git a/modules/swagger-core/src/main/java/io/swagger/v3/core/util/AnnotationsUtils.java b/modules/swagger-core/src/main/java/io/swagger/v3/core/util/AnnotationsUtils.java
index 7d758f4f10..18c8532f4c 100644
--- a/modules/swagger-core/src/main/java/io/swagger/v3/core/util/AnnotationsUtils.java
+++ b/modules/swagger-core/src/main/java/io/swagger/v3/core/util/AnnotationsUtils.java
@@ -43,11 +43,16 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.io.ByteArrayOutputStream;
import java.io.IOException;
+import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@@ -432,6 +437,42 @@ public static Optional getExample(ExampleObject example, boolean ignore
return Optional.empty();
}
+ private static String loadExternalFile(String path) {
+ if (StringUtils.isBlank(path)) {
+ return null;
+ }
+ try {
+ if (path.startsWith("file:")) {
+ return new String(Files.readAllBytes(Paths.get(path.substring("file:".length()))), StandardCharsets.UTF_8);
+ } else {
+ String resourcePath = path.startsWith("classpath:") ? path.substring("classpath:".length()) : path;
+ if (resourcePath.startsWith("/")) {
+ resourcePath = resourcePath.substring(1);
+ }
+ ClassLoader cl = Thread.currentThread().getContextClassLoader();
+ if (cl == null) {
+ cl = AnnotationsUtils.class.getClassLoader();
+ }
+ try (InputStream is = cl.getResourceAsStream(resourcePath)) {
+ if (is == null) {
+ LOGGER.warn("Could not find classpath resource for @ExampleObject externalFile: {}", path);
+ return null;
+ }
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ byte[] chunk = new byte[8192];
+ int bytesRead;
+ while ((bytesRead = is.read(chunk)) != -1) {
+ buffer.write(chunk, 0, bytesRead);
+ }
+ return buffer.toString(StandardCharsets.UTF_8.name());
+ }
+ }
+ } catch (Exception e) {
+ LOGGER.warn("Failed to load @ExampleObject externalFile '{}': {}", path, e.getMessage());
+ return null;
+ }
+ }
+
private static boolean resolveExample(Example exampleObject, ExampleObject example) {
boolean isEmpty = true;
@@ -449,13 +490,24 @@ private static boolean resolveExample(Example exampleObject, ExampleObject examp
isEmpty = false;
exampleObject.setExternalValue(example.externalValue());
}
- if (StringUtils.isNotBlank(example.value())) {
+ // externalFile takes precedence over value; falls back to value if file not found
+ String effectiveValue = null;
+ if (StringUtils.isNotBlank(example.externalFile())) {
+ effectiveValue = loadExternalFile(example.externalFile());
+ if (effectiveValue != null) {
+ isEmpty = false;
+ }
+ }
+ if (effectiveValue == null && StringUtils.isNotBlank(example.value())) {
+ effectiveValue = example.value();
isEmpty = false;
+ }
+ if (effectiveValue != null) {
try {
ObjectMapper mapper = ObjectMapperFactory.buildStrictGenericObjectMapper();
- exampleObject.setValue(mapper.readTree(example.value()));
+ exampleObject.setValue(mapper.readTree(effectiveValue));
} catch (IOException e) {
- exampleObject.setValue(example.value());
+ exampleObject.setValue(effectiveValue);
}
}
if (StringUtils.isNotBlank(example.ref())) {
diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/util/AnnotationsUtilsTest.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/util/AnnotationsUtilsTest.java
index 7c3063ac79..cb23c69aa1 100644
--- a/modules/swagger-core/src/test/java/io/swagger/v3/core/util/AnnotationsUtilsTest.java
+++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/util/AnnotationsUtilsTest.java
@@ -10,6 +10,7 @@
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.DependentRequired;
import io.swagger.v3.oas.annotations.media.DiscriminatorMapping;
+import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.media.Schema;
@@ -33,6 +34,7 @@
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotEquals;
+import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;
import static org.testng.AssertJUnit.assertNull;
@@ -954,4 +956,54 @@ public void sentinelShouldNeverAppearInResolvedSchema() throws Exception {
"Sentinel value must never appear in resolved schema default");
}
+ @Test
+ public void testGetExampleWithExternalClasspathFile() {
+ ExampleObject exampleObject = buildExampleObject("testExampleFile.json", "");
+ Optional result = AnnotationsUtils.getExample(exampleObject, true);
+ assertTrue(result.isPresent());
+ assertNotNull(result.get().getValue());
+ assertTrue(result.get().getValue() instanceof JsonNode, "File content should be parsed as JsonNode");
+ }
+
+ @Test
+ public void testGetExampleExternalFilePrecedenceOverValue() {
+ ExampleObject exampleObject = buildExampleObject("testExampleFile.json", "{\"override\": true}");
+ Optional result = AnnotationsUtils.getExample(exampleObject, true);
+ assertTrue(result.isPresent());
+ assertFalse(result.get().getValue().toString().contains("override"),
+ "externalFile should take precedence over value when file is found");
+ }
+
+ @Test
+ public void testGetExampleMissingExternalFileFallsBackToValue() {
+ ExampleObject exampleObject = buildExampleObject("nonexistent-file.json", "{\"fallback\": true}");
+ Optional result = AnnotationsUtils.getExample(exampleObject, true);
+ assertTrue(result.isPresent());
+ assertTrue(result.get().getValue().toString().contains("fallback"),
+ "When externalFile is missing, value should be used as fallback");
+ }
+
+ @Test
+ public void testGetExampleWithClasspathPrefix() {
+ ExampleObject exampleObject = buildExampleObject("classpath:testExampleFile.json", "");
+ Optional result = AnnotationsUtils.getExample(exampleObject, true);
+ assertTrue(result.isPresent());
+ assertNotNull(result.get().getValue());
+ assertTrue(result.get().getValue() instanceof JsonNode, "classpath: prefixed file should be parsed as JsonNode");
+ }
+
+ private ExampleObject buildExampleObject(String externalFile, String value) {
+ return new ExampleObject() {
+ @Override public Class extends Annotation> annotationType() { return ExampleObject.class; }
+ @Override public String name() { return "test"; }
+ @Override public String summary() { return ""; }
+ @Override public String value() { return value; }
+ @Override public String externalValue() { return ""; }
+ @Override public Extension[] extensions() { return new Extension[0]; }
+ @Override public String ref() { return ""; }
+ @Override public String description() { return ""; }
+ @Override public String externalFile() { return externalFile; }
+ };
+ }
+
}
diff --git a/modules/swagger-core/src/test/resources/testExampleFile.json b/modules/swagger-core/src/test/resources/testExampleFile.json
new file mode 100644
index 0000000000..96a6f3e005
--- /dev/null
+++ b/modules/swagger-core/src/test/resources/testExampleFile.json
@@ -0,0 +1 @@
+{"id": 1}
diff --git a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/annotations/examples/ExamplesTest.java b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/annotations/examples/ExamplesTest.java
index 423455dbdc..a97461d646 100644
--- a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/annotations/examples/ExamplesTest.java
+++ b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/annotations/examples/ExamplesTest.java
@@ -50,6 +50,11 @@ public void testAnnotatedModel() {
compareToYamlFile(ExamplesTest.AnnotatedModelAndContentExample.class, "examples/");
}
+ @Test
+ public void testExternalFileExample() {
+ compareToYamlFile(ExamplesTest.ExternalFileRequestBodyExample.class, "examples/");
+ }
+
static class ResponseExampleSchema {
@Path("/test")
@POST
@@ -492,6 +497,26 @@ public ExamplesTest.SubscriptionResponse subscribe(@RequestBody(description = "C
}
}
+ static class ExternalFileRequestBodyExample {
+ @Path("/test")
+ @POST
+ public Subscription testRequestBody(
+ @RequestBody(
+ description = "Created user object",
+ required = true,
+ content = @Content(
+ examples = {
+ @ExampleObject(
+ name = "Default Request",
+ externalFile = "examples/external/user-request.json",
+ summary = "Subscription Example from file")
+ }
+ )
+ ) Subscription sub) {
+ return null;
+ }
+ }
+
static class SubscriptionResponse {
public String subscriptionId;
}
diff --git a/modules/swagger-jaxrs2/src/test/resources/examples/ExternalFileRequestBodyExample.yaml b/modules/swagger-jaxrs2/src/test/resources/examples/ExternalFileRequestBodyExample.yaml
new file mode 100644
index 0000000000..94234e05b3
--- /dev/null
+++ b/modules/swagger-jaxrs2/src/test/resources/examples/ExternalFileRequestBodyExample.yaml
@@ -0,0 +1,41 @@
+openapi: 3.0.1
+paths:
+ /test:
+ post:
+ operationId: testRequestBody
+ requestBody:
+ description: Created user object
+ content:
+ '*/*':
+ schema:
+ $ref: "#/components/schemas/Subscription"
+ examples:
+ Default Request:
+ summary: Subscription Example from file
+ description: Default Request
+ value:
+ subscriptionId: "1"
+ subscriptionItem:
+ subscriptionItemId: "2"
+ required: true
+ responses:
+ default:
+ description: default response
+ content:
+ '*/*':
+ schema:
+ $ref: "#/components/schemas/Subscription"
+components:
+ schemas:
+ SubscriptionItem:
+ type: object
+ properties:
+ subscriptionItemId:
+ type: string
+ Subscription:
+ type: object
+ properties:
+ subscriptionId:
+ type: string
+ subscriptionItem:
+ $ref: "#/components/schemas/SubscriptionItem"
diff --git a/modules/swagger-jaxrs2/src/test/resources/examples/external/user-request.json b/modules/swagger-jaxrs2/src/test/resources/examples/external/user-request.json
new file mode 100644
index 0000000000..1c9aa1dc0d
--- /dev/null
+++ b/modules/swagger-jaxrs2/src/test/resources/examples/external/user-request.json
@@ -0,0 +1 @@
+{"subscriptionId": "1", "subscriptionItem": {"subscriptionItemId": "2"}}