Repository: springdoc/springdoc-openapi
Pull request #438 (2020-02-17 12:09:53)

Add custom ModelConverter for Spring HATEOAS CollectionModel.
There is a custom serializer that converst the Collection to a Map. Thus the definition needs to be tweaked to match this.


Patch:
diff --git a/springdoc-openapi-common/src/main/java/org/springdoc/api/AbstractOpenApiResource.java b/springdoc-openapi-common/src/main/java/org/springdoc/api/AbstractOpenApiResource.java
index daac72fed..af3adddcc 100644
--- a/springdoc-openapi-common/src/main/java/org/springdoc/api/AbstractOpenApiResource.java
+++ b/springdoc-openapi-common/src/main/java/org/springdoc/api/AbstractOpenApiResource.java
@@ -189,6 +189,9 @@ public abstract class AbstractOpenApiResource extends SpecFilter {
 				operation.setDeprecated(true);
 			}
 
+			// compute tags
+			operation = openAPIBuilder.buildTags(handlerMethod, operation, openAPI);
+
 			// Add documentation from operation annotation
 			io.swagger.v3.oas.annotations.Operation apiOperation = ReflectionUtils.getAnnotation(method,
 					io.swagger.v3.oas.annotations.Operation.class);
@@ -199,9 +202,6 @@ public abstract class AbstractOpenApiResource extends SpecFilter {
 				openAPI = operationParser.parse(components, apiOperation, operation, openAPI, methodAttributes);
 			}
 
-			// compute tags
-			operation = openAPIBuilder.buildTags(handlerMethod, operation, openAPI);
-
 			io.swagger.v3.oas.annotations.parameters.RequestBody requestBodyDoc = ReflectionUtils.getAnnotation(method,
 					io.swagger.v3.oas.annotations.parameters.RequestBody.class);
 
diff --git a/springdoc-openapi-data-rest/src/main/java/org/springdoc/core/HalProvider.java b/springdoc-openapi-data-rest/src/main/java/org/springdoc/core/HalProvider.java
index d63342306..dd4364329 100644
--- a/springdoc-openapi-data-rest/src/main/java/org/springdoc/core/HalProvider.java
+++ b/springdoc-openapi-data-rest/src/main/java/org/springdoc/core/HalProvider.java
@@ -20,8 +20,10 @@ package org.springdoc.core;
 
 import javax.annotation.PostConstruct;
 
+import io.swagger.v3.core.converter.ModelConverters;
 import io.swagger.v3.core.util.Json;
 
+import org.springdoc.core.hal.CollectionModelContentConverter;
 import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
 import org.springframework.hateoas.mediatype.hal.Jackson2HalModule;
 
@@ -35,7 +37,10 @@ public class HalProvider {
 
 	@PostConstruct
 	private void init(){
-		if(repositoryRestConfiguration.useHalAsDefaultJsonMediaType())
+		if(repositoryRestConfiguration.useHalAsDefaultJsonMediaType()) {
 			Json.mapper().registerModule(new Jackson2HalModule());
+			ModelConverters.getInstance()
+					.addConverter(new CollectionModelContentConverter());
+		}
 	}
 }
diff --git a/springdoc-openapi-data-rest/src/main/java/org/springdoc/core/SpringDocDataRestConfiguration.java b/springdoc-openapi-data-rest/src/main/java/org/springdoc/core/SpringDocDataRestConfiguration.java
index 4dae837fc..d64383321 100644
--- a/springdoc-openapi-data-rest/src/main/java/org/springdoc/core/SpringDocDataRestConfiguration.java
+++ b/springdoc-openapi-data-rest/src/main/java/org/springdoc/core/SpringDocDataRestConfiguration.java
@@ -24,7 +24,6 @@ import io.swagger.v3.core.converter.AnnotatedType;
 import io.swagger.v3.core.converter.ModelConverters;
 import io.swagger.v3.core.converter.ResolvedSchema;
 import io.swagger.v3.core.util.Json;
-import io.swagger.v3.oas.annotations.media.Schema;
 import io.swagger.v3.oas.models.media.MapSchema;
 import io.swagger.v3.oas.models.media.ObjectSchema;
 import io.swagger.v3.oas.models.media.StringSchema;
@@ -32,6 +31,7 @@ import org.springdoc.core.converters.AdditionalModelsConverter;
 import org.springdoc.core.converters.Pageable;
 import org.springdoc.core.customizers.OpenApiCustomiser;
 
+import org.springdoc.core.hal.RepresentationModelLinksOASMixin;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
@@ -39,7 +39,6 @@ import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
 import org.springframework.hateoas.Link;
 import org.springframework.hateoas.Links;
 import org.springframework.hateoas.RepresentationModel;
-import org.springframework.hateoas.mediatype.hal.RepresentationModelMixin;
 
 import static org.springdoc.core.Constants.SPRINGDOC_ENABLED;
 
@@ -69,18 +68,14 @@ public class SpringDocDataRestConfiguration {
             return openApi -> {};
         }
         Json.mapper().addMixIn(RepresentationModel.class, RepresentationModelLinksOASMixin.class);
+
         ResolvedSchema resolvedLinkSchema = ModelConverters.getInstance()
                 .resolveAsResolvedSchema(new AnnotatedType(Link.class));
+
         return openApi -> openApi
 				.schema("Link", resolvedLinkSchema.schema)
 				.schema("Links", new MapSchema()
 						.additionalProperties(new StringSchema())
 						.additionalProperties(new ObjectSchema().$ref("#/components/schemas/Link")));
     }
-
-	abstract static class RepresentationModelLinksOASMixin extends RepresentationModelMixin {
-        @Override
-        @Schema(ref = "#/components/schemas/Links")
-        public abstract Links getLinks();
-    }
 }
diff --git a/springdoc-openapi-data-rest/src/main/java/org/springdoc/core/hal/CollectionModelContentConverter.java b/springdoc-openapi-data-rest/src/main/java/org/springdoc/core/hal/CollectionModelContentConverter.java
new file mode 100644
index 000000000..e104579f0
--- /dev/null
+++ b/springdoc-openapi-data-rest/src/main/java/org/springdoc/core/hal/CollectionModelContentConverter.java
@@ -0,0 +1,38 @@
+package org.springdoc.core.hal;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.type.CollectionType;
+import io.swagger.v3.core.converter.AnnotatedType;
+import io.swagger.v3.core.converter.ModelConverter;
+import io.swagger.v3.core.converter.ModelConverterContext;
+import io.swagger.v3.oas.models.media.ArraySchema;
+import io.swagger.v3.oas.models.media.MapSchema;
+import io.swagger.v3.oas.models.media.Schema;
+import io.swagger.v3.oas.models.media.StringSchema;
+
+import java.util.Collection;
+import java.util.Iterator;
+
+/**
+ * Override resolved schema as there is a custom serializer that converts the data to a map before serializing it.
+ *
+ * @see org.springframework.hateoas.mediatype.hal.Jackson2HalModule.HalResourcesSerializer
+ * @see org.springframework.hateoas.mediatype.hal.Jackson2HalModule.HalResourcesSerializer#serialize(Collection, JsonGenerator, SerializerProvider)
+ */
+public class CollectionModelContentConverter implements ModelConverter {
+    @Override
+    public Schema<?> resolve(AnnotatedType type, ModelConverterContext context, Iterator<ModelConverter> chain) {
+        if (chain.hasNext() && type != null && type.getType() instanceof CollectionType
+                && "_embedded".equalsIgnoreCase(type.getPropertyName())) {
+            Schema<?> schema = chain.next().resolve(type, context, chain);
+            if (schema instanceof ArraySchema) {
+                return new MapSchema()
+                        .name("_embedded")
+                        .additionalProperties(new StringSchema())
+                        .additionalProperties(schema);
+            }
+        }
+        return chain.hasNext() ? chain.next().resolve(type, context, chain) : null;
+    }
+}
diff --git a/springdoc-openapi-data-rest/src/main/java/org/springdoc/core/hal/RepresentationModelLinksOASMixin.java b/springdoc-openapi-data-rest/src/main/java/org/springdoc/core/hal/RepresentationModelLinksOASMixin.java
new file mode 100644
index 000000000..840fa539e
--- /dev/null
+++ b/springdoc-openapi-data-rest/src/main/java/org/springdoc/core/hal/RepresentationModelLinksOASMixin.java
@@ -0,0 +1,11 @@
+package org.springdoc.core.hal;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import org.springframework.hateoas.Links;
+import org.springframework.hateoas.mediatype.hal.RepresentationModelMixin;
+
+public abstract class RepresentationModelLinksOASMixin extends RepresentationModelMixin {
+    @Override
+    @Schema(ref = "#/components/schemas/Links")
+    public abstract Links getLinks();
+}


Test patch:
diff --git a/springdoc-openapi-webmvc-core/src/test/resources/results/app1.json b/springdoc-openapi-webmvc-core/src/test/resources/results/app1.json
index ec09890a2..e63420fb2 100644
--- a/springdoc-openapi-webmvc-core/src/test/resources/results/app1.json
+++ b/springdoc-openapi-webmvc-core/src/test/resources/results/app1.json
@@ -66,7 +66,8 @@
     "/inventory": {
       "get": {
         "tags": [
-          "inventory"
+          "inventory",
+          "developers"
         ],
         "summary": "By passing in the appropriate options, you can search for available inventory in the system ",
         "description": "searches inventory",
@@ -146,7 +147,8 @@
       },
       "post": {
         "tags": [
-          "inventory"
+          "inventory",
+          "admins"
         ],
         "summary": "Adds an item to the system",
         "description": "adds an inventory item",
@@ -286,40 +288,6 @@
         }
       }
     },
-    "/people": {
-      "get": {
-        "tags": [
-          "people"
-        ],
-        "description": "List all people",
-        "operationId": "getPeople",
-        "responses": {
-          "500": {
-            "description": "default response",
-            "content": {
-              "*/*": {
-                "schema": {
-                  "$ref": "#/components/schemas/ErrorMessage"
-                }
-              }
-            }
-          },
-          "200": {
-            "description": "default response",
-            "content": {
-              "application/json": {
-                "schema": {
-                  "type": "array",
-                  "items": {
-                    "$ref": "#/components/schemas/PersonDTO"
-                  }
-                }
-              }
-            }
-          }
-        }
-      }
-    },
     "/{email}": {
       "get": {
         "tags": [
@@ -339,18 +307,18 @@
           }
         ],
         "responses": {
-          "500": {
+          "200": {
             "description": "default response",
             "content": {
-              "*/*": {
+              "application/json": {
                 "schema": {
-                  "$ref": "#/components/schemas/ErrorMessage"
+                  "$ref": "#/components/schemas/PersonDTO"
                 }
               }
             }
           },
-          "200": {
-            "description": "default response",
+          "404": {
+            "description": "Person with such e-mail doesn't exists",
             "content": {
               "application/json": {
                 "schema": {
@@ -359,12 +327,12 @@
               }
             }
           },
-          "404": {
-            "description": "Person with such e-mail doesn't exists",
+          "500": {
+            "description": "default response",
             "content": {
-              "application/json": {
+              "*/*": {
                 "schema": {
-                  "$ref": "#/components/schemas/PersonDTO"
+                  "$ref": "#/components/schemas/ErrorMessage"
                 }
               }
             }
@@ -407,16 +375,6 @@
           }
         ],
         "responses": {
-          "500": {
-            "description": "default response",
-            "content": {
-              "*/*": {
-                "schema": {
-                  "$ref": "#/components/schemas/ErrorMessage"
-                }
-              }
-            }
-          },
           "201": {
             "description": "default response",
             "headers": {
@@ -444,6 +402,16 @@
                 }
               }
             }
+          },
+          "500": {
+            "description": "default response",
+            "content": {
+              "*/*": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorMessage"
+                }
+              }
+            }
           }
         }
       },
@@ -465,18 +433,18 @@
           }
         ],
         "responses": {
-          "500": {
-            "description": "default response",
+          "204": {
+            "description": "Person has been deleted",
             "content": {
               "*/*": {
                 "schema": {
-                  "$ref": "#/components/schemas/ErrorMessage"
+                  "type": "string"
                 }
               }
             }
           },
-          "204": {
-            "description": "Person has been deleted",
+          "404": {
+            "description": "Person with such e-mail doesn't exists",
             "content": {
               "*/*": {
                 "schema": {
@@ -485,12 +453,46 @@
               }
             }
           },
-          "404": {
-            "description": "Person with such e-mail doesn't exists",
+          "500": {
+            "description": "default response",
             "content": {
               "*/*": {
                 "schema": {
-                  "type": "string"
+                  "$ref": "#/components/schemas/ErrorMessage"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/people": {
+      "get": {
+        "tags": [
+          "people"
+        ],
+        "description": "List all people",
+        "operationId": "getPeople",
+        "responses": {
+          "200": {
+            "description": "default response",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "array",
+                  "items": {
+                    "$ref": "#/components/schemas/PersonDTO"
+                  }
+                }
+              }
+            }
+          },
+          "500": {
+            "description": "default response",
+            "content": {
+              "*/*": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorMessage"
                 }
               }
             }
@@ -559,9 +561,12 @@
           }
         }
       },
-      "ItemLightDTO": {
+      "ItemDTO": {
         "type": "object",
         "properties": {
+          "itemID": {
+            "type": "string"
+          },
           "description": {
             "type": "string"
           },
@@ -571,12 +576,9 @@
           }
         }
       },
-      "ItemDTO": {
+      "ItemLightDTO": {
         "type": "object",
         "properties": {
-          "itemID": {
-            "type": "string"
-          },
           "description": {
             "type": "string"
           },
diff --git a/springdoc-openapi-webmvc-core/src/test/resources/results/app21.json b/springdoc-openapi-webmvc-core/src/test/resources/results/app21.json
index e30069fcc..8fbde4a62 100644
--- a/springdoc-openapi-webmvc-core/src/test/resources/results/app21.json
+++ b/springdoc-openapi-webmvc-core/src/test/resources/results/app21.json
@@ -14,6 +14,7 @@
     "/persons": {
       "get": {
         "tags": [
+          "hello-controller",
           "person"
         ],
         "summary": "Add a new person to the store",
diff --git a/springdoc-openapi-webmvc-core/src/test/resources/results/app46.json b/springdoc-openapi-webmvc-core/src/test/resources/results/app46.json
index 3fd699107..9f233c9bf 100644
--- a/springdoc-openapi-webmvc-core/src/test/resources/results/app46.json
+++ b/springdoc-openapi-webmvc-core/src/test/resources/results/app46.json
@@ -14,6 +14,7 @@
     "/persons/{subscriptionId}": {
       "get": {
         "tags": [
+          "hello-controller",
           "Example Tag"
         ],
         "summary": "Operation Summary",