diff --git a/modules/swagger-core/src/main/java/io/swagger/v3/core/jackson/mixin/PathItemMixin.java b/modules/swagger-core/src/main/java/io/swagger/v3/core/jackson/mixin/PathItemMixin.java
new file mode 100644
index 0000000000..7e1ddf4b84
--- /dev/null
+++ b/modules/swagger-core/src/main/java/io/swagger/v3/core/jackson/mixin/PathItemMixin.java
@@ -0,0 +1,26 @@
+package io.swagger.v3.core.jackson.mixin;
+
+import com.fasterxml.jackson.annotation.JsonAnyGetter;
+import com.fasterxml.jackson.annotation.JsonAnySetter;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import io.swagger.v3.oas.models.Operation;
+
+import java.util.Map;
+
+/**
+ * Mixin applied to {@link io.swagger.v3.oas.models.PathItem} for OpenAPI 3.0 and 3.1 mappers.
+ *
+ *
The {@code query} method is a fixed field of the Path Item Object only as of OpenAPI 3.2,
+ * so it is ignored here to keep 3.0 and 3.1 documents conformant.
+ */
+public abstract class PathItemMixin {
+
+ @JsonAnyGetter
+ public abstract Map getExtensions();
+
+ @JsonAnySetter
+ public abstract void addExtension(String name, Object value);
+
+ @JsonIgnore
+ public abstract Operation getQuery();
+}
diff --git a/modules/swagger-core/src/main/java/io/swagger/v3/core/util/ObjectMapperFactory.java b/modules/swagger-core/src/main/java/io/swagger/v3/core/util/ObjectMapperFactory.java
index a02539f54c..4eefee28ca 100644
--- a/modules/swagger-core/src/main/java/io/swagger/v3/core/util/ObjectMapperFactory.java
+++ b/modules/swagger-core/src/main/java/io/swagger/v3/core/util/ObjectMapperFactory.java
@@ -31,6 +31,7 @@
import io.swagger.v3.core.jackson.mixin.OpenAPI31Mixin;
import io.swagger.v3.core.jackson.mixin.OpenAPIMixin;
import io.swagger.v3.core.jackson.mixin.OperationMixin;
+import io.swagger.v3.core.jackson.mixin.PathItemMixin;
import io.swagger.v3.core.jackson.mixin.Schema31Mixin;
import io.swagger.v3.core.jackson.mixin.SchemaConverterMixin;
import io.swagger.v3.core.jackson.mixin.SchemaMixin;
@@ -186,7 +187,7 @@ public JsonSerializer> modifySerializer(
sourceMixins.put(OAuthFlow.class, ExtensionsMixin.class);
sourceMixins.put(OAuthFlows.class, ExtensionsMixin.class);
sourceMixins.put(Operation.class, OperationMixin.class);
- sourceMixins.put(PathItem.class, ExtensionsMixin.class);
+ sourceMixins.put(PathItem.class, PathItemMixin.class);
sourceMixins.put(Paths.class, ExtensionsMixin.class);
sourceMixins.put(Scopes.class, ExtensionsMixin.class);
sourceMixins.put(Server.class, ExtensionsMixin.class);
@@ -261,7 +262,7 @@ public static ObjectMapper createJsonConverter() {
sourceMixins.put(OpenAPI.class, OpenAPIMixin.class);
sourceMixins.put(Operation.class, OperationMixin.class);
sourceMixins.put(Parameter.class, ExtensionsMixin.class);
- sourceMixins.put(PathItem.class, ExtensionsMixin.class);
+ sourceMixins.put(PathItem.class, PathItemMixin.class);
sourceMixins.put(Paths.class, ExtensionsMixin.class);
sourceMixins.put(RequestBody.class, ExtensionsMixin.class);
sourceMixins.put(Scopes.class, ExtensionsMixin.class);
diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/serialization/JsonSerializationTest.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/serialization/JsonSerializationTest.java
index 29163d0cca..68594eb0f6 100644
--- a/modules/swagger-core/src/test/java/io/swagger/v3/core/serialization/JsonSerializationTest.java
+++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/serialization/JsonSerializationTest.java
@@ -5,6 +5,7 @@
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import io.swagger.v3.core.matchers.SerializationMatchers;
import io.swagger.v3.core.util.Json;
+import io.swagger.v3.core.util.Json31;
import io.swagger.v3.core.util.ObjectMapperFactory;
import io.swagger.v3.core.util.ResourceUtils;
import io.swagger.v3.core.util.Yaml;
@@ -23,6 +24,9 @@
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
public class JsonSerializationTest {
@@ -42,6 +46,46 @@ public void testSerializeASpecWithPathReferences() throws Exception {
assertEquals(path, expectedPath);
}
+ @Test
+ public void testQueryHttpMethodIsNotSerializedBeforeOpenAPI32() throws Exception {
+
+ Operation queryOperation = new Operation()
+ .operationId("searchPets")
+ .responses(new ApiResponses()
+ .addApiResponse("200", new ApiResponse().description("ok")));
+
+ PathItem pathItem = new PathItem()
+ .get(new Operation().operationId("listPets"))
+ .query(queryOperation);
+ OpenAPI openAPI = new OpenAPI().path("/pets", pathItem);
+
+ // the model holds the operation ...
+ assertNotNull(openAPI.getPaths().get("/pets").getQuery());
+
+ // ... but "query" is a Path Item fixed field only as of OpenAPI 3.2, so neither the
+ // 3.0 nor the 3.1 mapper may emit it, while the other methods serialize as usual
+ for (String json : new String[]{
+ Json.mapper().writeValueAsString(openAPI),
+ Json31.mapper().writeValueAsString(openAPI)}) {
+ assertFalse(json.contains("\"query\""));
+ assertTrue(json.contains("\"listPets\""));
+ assertFalse(json.contains("\"searchPets\""));
+ }
+ }
+
+ @Test
+ public void testQueryHttpMethodIsNotDeserializedBeforeOpenAPI32() throws Exception {
+
+ String json = "{\"openapi\":\"3.0.1\",\"paths\":{\"/pets\":{"
+ + "\"get\":{\"operationId\":\"listPets\"},"
+ + "\"query\":{\"operationId\":\"searchPets\"}}}}";
+
+ PathItem pathItem = Json.mapper().readValue(json, OpenAPI.class).getPaths().get("/pets");
+
+ assertNotNull(pathItem.getGet());
+ assertNull(pathItem.getQuery());
+ }
+
@Test
public void testExtension() throws Exception {
diff --git a/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/Reader.java b/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/Reader.java
index b15115aaf7..309b0be8a2 100644
--- a/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/Reader.java
+++ b/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/Reader.java
@@ -99,6 +99,7 @@ public class Reader implements OpenApiReader {
private static final String TRACE_METHOD = "trace";
private static final String HEAD_METHOD = "head";
private static final String OPTIONS_METHOD = "options";
+ private static final String QUERY_METHOD = "query";
public Reader() {
this(new OpenAPI(), new Paths(), new LinkedHashSet<>(), new Components());
@@ -1397,6 +1398,9 @@ private void setPathItemOperation(PathItem pathItemObject, String method, Operat
case OPTIONS_METHOD:
pathItemObject.options(operation);
break;
+ case QUERY_METHOD:
+ pathItemObject.query(operation);
+ break;
default:
// Do nothing here
break;
@@ -1565,6 +1569,9 @@ private Set extractOperationIdFromPathItem(PathItem path) {
if (path.getPatch() != null && StringUtils.isNotBlank(path.getPatch().getOperationId())) {
ids.add(path.getPatch().getOperationId());
}
+ if (path.getQuery() != null && StringUtils.isNotBlank(path.getQuery().getOperationId())) {
+ ids.add(path.getQuery().getOperationId());
+ }
return ids;
}
diff --git a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/QueryMethodTest.java b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/QueryMethodTest.java
new file mode 100644
index 0000000000..b95ce019d3
--- /dev/null
+++ b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/QueryMethodTest.java
@@ -0,0 +1,55 @@
+package io.swagger.v3.jaxrs2;
+
+import io.swagger.v3.core.util.Json;
+import io.swagger.v3.core.util.Json31;
+import io.swagger.v3.core.util.Yaml;
+import io.swagger.v3.core.util.Yaml31;
+import io.swagger.v3.jaxrs2.resources.QueryMethodResource;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.PathItem;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+
+/**
+ * Verifies that the reader maps a custom {@code @HttpMethod("QUERY")} annotation
+ * (OpenAPI 3.2 query method) onto {@link PathItem#getQuery()}.
+ */
+public class QueryMethodTest {
+
+ @Test(description = "read a resource declaring the OpenAPI 3.2 QUERY method")
+ public void testQueryHttpMethod() {
+ Reader reader = new Reader(new OpenAPI());
+ OpenAPI openAPI = reader.read(QueryMethodResource.class);
+
+ PathItem pathItem = openAPI.getPaths().get("/pets/search");
+ assertNotNull(pathItem);
+
+ assertNotNull(pathItem.getQuery());
+ assertEquals(pathItem.getQuery().getOperationId(), "searchPets");
+
+ // the operation must land on query only, not leak onto other methods
+ assertNull(pathItem.getGet());
+ assertNull(pathItem.getPost());
+ }
+
+ @Test(description = "a QUERY operation must not reach OpenAPI 3.0 or 3.1 output")
+ public void testQueryHttpMethodIsNotSerializedBeforeOpenAPI32() throws Exception {
+ Reader reader = new Reader(new OpenAPI());
+ OpenAPI openAPI = reader.read(QueryMethodResource.class);
+
+ // "query" is a Path Item fixed field only as of OpenAPI 3.2, so the operation the
+ // reader resolved stays in the model but is gated out of pre-3.2 documents
+ for (String serialized : new String[]{
+ Yaml.mapper().writeValueAsString(openAPI),
+ Yaml31.mapper().writeValueAsString(openAPI),
+ Json.mapper().writeValueAsString(openAPI),
+ Json31.mapper().writeValueAsString(openAPI)}) {
+ assertFalse(serialized.contains("query"));
+ assertFalse(serialized.contains("searchPets"));
+ }
+ }
+}
diff --git a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/resources/QUERY.java b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/resources/QUERY.java
new file mode 100644
index 0000000000..f390ae81ac
--- /dev/null
+++ b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/resources/QUERY.java
@@ -0,0 +1,23 @@
+package io.swagger.v3.jaxrs2.resources;
+
+import javax.ws.rs.HttpMethod;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Stands in for the {@code @QUERY} binding an application declares itself, since JAX-RS /
+ * Jakarta REST does not ship one for the OpenAPI 3.2 {@code QUERY} method.
+ *
+ * It is meta-annotated with {@link HttpMethod} in the same way {@code javax.ws.rs.PATCH} is,
+ * which is all the reader needs: {@code ReaderUtils.getHttpMethodFromCustomAnnotations} resolves
+ * any such annotation to its lower-cased method name, which is then mapped onto
+ * {@link io.swagger.v3.oas.models.PathItem#getQuery()}.
+ */
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@HttpMethod("QUERY")
+public @interface QUERY {
+}
diff --git a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/resources/QueryMethodResource.java b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/resources/QueryMethodResource.java
new file mode 100644
index 0000000000..23cea774bc
--- /dev/null
+++ b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/resources/QueryMethodResource.java
@@ -0,0 +1,19 @@
+package io.swagger.v3.jaxrs2.resources;
+
+import io.swagger.v3.oas.annotations.Operation;
+
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Response;
+
+@Path("/pets")
+public class QueryMethodResource {
+
+ @QUERY
+ @Path("/search")
+ @Produces("application/json")
+ @Operation(operationId = "searchPets", summary = "Search pets")
+ public Response searchPets() {
+ return Response.ok().build();
+ }
+}
diff --git a/modules/swagger-models/src/main/java/io/swagger/v3/oas/models/PathItem.java b/modules/swagger-models/src/main/java/io/swagger/v3/oas/models/PathItem.java
index 4b6acf8d80..16631a9d0e 100644
--- a/modules/swagger-models/src/main/java/io/swagger/v3/oas/models/PathItem.java
+++ b/modules/swagger-models/src/main/java/io/swagger/v3/oas/models/PathItem.java
@@ -14,6 +14,7 @@
*
* @see PathItem (OpenAPI 3.0 specification)
* @see PathItem (OpenAPI 3.1 specification)
+ * @see PathItem (OpenAPI 3.2 specification)
*/
public class PathItem {
@@ -27,6 +28,7 @@ public class PathItem {
private Operation head = null;
private Operation patch = null;
private Operation trace = null;
+ private Operation query = null;
private List servers = null;
private List parameters = null;
private String $ref = null;
@@ -222,6 +224,30 @@ public PathItem trace(Operation trace) {
return this;
}
+ /**
+ * returns the query property from a PathItem instance.
+ *
+ * The {@code query} HTTP method was introduced in OpenAPI 3.2. It is therefore not
+ * serialized by OpenAPI 3.0 and 3.1 mappers, which would otherwise emit a Path Item
+ * field that is not valid for those versions.
+ *
+ * @return Operation query
+ * @since OpenAPI 3.2
+ **/
+
+ public Operation getQuery() {
+ return query;
+ }
+
+ public void setQuery(Operation query) {
+ this.query = query;
+ }
+
+ public PathItem query(Operation query) {
+ this.query = query;
+ return this;
+ }
+
public List readOperations() {
List allOperations = new ArrayList<>();
if (this.get != null) {
@@ -248,6 +274,9 @@ public List readOperations() {
if (this.trace != null) {
allOperations.add(this.trace);
}
+ if (this.query != null) {
+ allOperations.add(this.query);
+ }
return allOperations;
}
@@ -278,6 +307,9 @@ public void operation(HttpMethod method, Operation operation) {
case DELETE:
this.delete = operation;
break;
+ case QUERY:
+ this.query = operation;
+ break;
default:
}
}
@@ -290,7 +322,8 @@ public enum HttpMethod {
DELETE,
HEAD,
OPTIONS,
- TRACE
+ TRACE,
+ QUERY
}
public Map readOperationsMap() {
@@ -320,6 +353,9 @@ public Map readOperationsMap() {
if (this.trace != null) {
result.put(HttpMethod.TRACE, this.trace);
}
+ if (this.query != null) {
+ result.put(HttpMethod.QUERY, this.query);
+ }
return result;
}
@@ -468,6 +504,9 @@ public boolean equals(Object o) {
if (trace != null ? !trace.equals(pathItem.trace) : pathItem.trace != null) {
return false;
}
+ if (query != null ? !query.equals(pathItem.query) : pathItem.query != null) {
+ return false;
+ }
if (servers != null ? !servers.equals(pathItem.servers) : pathItem.servers != null) {
return false;
}
@@ -493,6 +532,7 @@ public int hashCode() {
result = 31 * result + (head != null ? head.hashCode() : 0);
result = 31 * result + (patch != null ? patch.hashCode() : 0);
result = 31 * result + (trace != null ? trace.hashCode() : 0);
+ result = 31 * result + (query != null ? query.hashCode() : 0);
result = 31 * result + (servers != null ? servers.hashCode() : 0);
result = 31 * result + (parameters != null ? parameters.hashCode() : 0);
result = 31 * result + ($ref != null ? $ref.hashCode() : 0);
@@ -514,6 +554,7 @@ public String toString() {
sb.append(" head: ").append(toIndentedString(head)).append("\n");
sb.append(" patch: ").append(toIndentedString(patch)).append("\n");
sb.append(" trace: ").append(toIndentedString(trace)).append("\n");
+ sb.append(" query: ").append(toIndentedString(query)).append("\n");
sb.append(" servers: ").append(toIndentedString(servers)).append("\n");
sb.append(" parameters: ").append(toIndentedString(parameters)).append("\n");
sb.append(" $ref: ").append(toIndentedString($ref)).append("\n");
diff --git a/modules/swagger-models/src/test/java/io/swagger/v3/oas/models/PathItemTest.java b/modules/swagger-models/src/test/java/io/swagger/v3/oas/models/PathItemTest.java
new file mode 100644
index 0000000000..b3d27656f9
--- /dev/null
+++ b/modules/swagger-models/src/test/java/io/swagger/v3/oas/models/PathItemTest.java
@@ -0,0 +1,80 @@
+package io.swagger.v3.oas.models;
+
+import org.testng.annotations.Test;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotEquals;
+import static org.testng.Assert.assertSame;
+import static org.testng.Assert.assertTrue;
+
+/**
+ * Tests for the OpenAPI 3.2 {@code query} HTTP method support on {@link PathItem}.
+ */
+public class PathItemTest {
+
+ @Test
+ public void testQueryBuilderAndAccessors() {
+ Operation query = new Operation();
+ PathItem pathItem = new PathItem().query(query);
+
+ assertSame(pathItem.getQuery(), query);
+
+ Operation other = new Operation();
+ pathItem.setQuery(other);
+ assertSame(pathItem.getQuery(), other);
+ }
+
+ @Test
+ public void testOperationSetterWithQueryMethod() {
+ Operation query = new Operation();
+ PathItem pathItem = new PathItem();
+ pathItem.operation(PathItem.HttpMethod.QUERY, query);
+
+ assertSame(pathItem.getQuery(), query);
+ }
+
+ @Test
+ public void testHttpMethodEnumContainsQuery() {
+ assertEquals(PathItem.HttpMethod.valueOf("QUERY"), PathItem.HttpMethod.QUERY);
+ }
+
+ @Test
+ public void testReadOperationsIncludesQuery() {
+ Operation query = new Operation();
+ PathItem pathItem = new PathItem().query(query);
+
+ List operations = pathItem.readOperations();
+ assertTrue(operations.contains(query));
+ }
+
+ @Test
+ public void testReadOperationsMapIncludesQuery() {
+ Operation query = new Operation();
+ PathItem pathItem = new PathItem().query(query);
+
+ Map map = pathItem.readOperationsMap();
+ assertSame(map.get(PathItem.HttpMethod.QUERY), query);
+ }
+
+ @Test
+ public void testEqualsAndHashCodeConsiderQuery() {
+ Operation query = new Operation().operationId("search");
+
+ PathItem passedObj = new PathItem().query(query);
+ PathItem createdObj = new PathItem().query(new Operation().operationId("search"));
+ PathItem emptyObj = new PathItem();
+
+ assertEquals(passedObj, createdObj);
+ assertEquals(passedObj.hashCode(), createdObj.hashCode());
+ assertNotEquals(passedObj, emptyObj);
+ }
+
+ @Test
+ public void testToStringContainsQuery() {
+ PathItem pathItem = new PathItem().query(new Operation());
+ assertTrue(pathItem.toString().contains("query:"));
+ }
+}