Skip to content

Commit

Permalink
Fixed wrong schema when schema set by RequestBody annotation
Browse files Browse the repository at this point in the history
  • Loading branch information
altro3 committed Jun 19, 2023
1 parent ea06e90 commit 2629523
Show file tree
Hide file tree
Showing 5 changed files with 214 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@
import static io.micronaut.openapi.visitor.ElementUtils.isNullable;
import static io.micronaut.openapi.visitor.OpenApiApplicationVisitor.getSecurityProperties;
import static io.micronaut.openapi.visitor.OpenApiApplicationVisitor.isOpenApiEnabled;
import static io.micronaut.openapi.visitor.SchemaUtils.COMPONENTS_CALLBACKS_PREFIX;
import static io.micronaut.openapi.visitor.SchemaUtils.COMPONENTS_SCHEMAS_PREFIX;
import static io.micronaut.openapi.visitor.SchemaUtils.TYPE_OBJECT;
import static io.micronaut.openapi.visitor.Utils.DEFAULT_MEDIA_TYPES;

Expand All @@ -122,8 +124,6 @@
*/
public abstract class AbstractOpenApiEndpointVisitor extends AbstractOpenApiVisitor {

public static final String COMPONENTS_CALLBACKS_PREFIX = "#/components/callbacks/";

protected static final String CONTEXT_CHILD_PATH = "internal.child.path";
protected static final String CONTEXT_CHILD_OP_ID_PREFIX = "internal.opId.prefix";
protected static final String CONTEXT_CHILD_OP_ID_SUFFIX = "internal.opId.suffix";
Expand Down Expand Up @@ -398,6 +398,8 @@ public void visitMethod(MethodElement element, VisitorContext context) {
List<MediaType> consumesMediaTypes = consumesMediaTypes(element);
Map<PathItem, io.swagger.v3.oas.models.Operation> swaggerOperations = readOperations(pathItemEntry.getKey(), httpMethod, pathItems, element, context);

boolean isRequestBodySchemaSet = false;

for (Map.Entry<PathItem, io.swagger.v3.oas.models.Operation> operationEntry : swaggerOperations.entrySet()) {
io.swagger.v3.oas.models.Operation swaggerOperation = operationEntry.getValue();
io.swagger.v3.oas.models.ExternalDocumentation externalDocs = readExternalDocs(element, context);
Expand Down Expand Up @@ -429,6 +431,11 @@ public void visitMethod(MethodElement element, VisitorContext context) {
if (permitsRequestBody) {
RequestBody requestBody = readSwaggerRequestBody(element, consumesMediaTypes, context);
if (requestBody != null) {
if (requestBody.getContent() != null) {
for (Map.Entry<String, io.swagger.v3.oas.models.media.MediaType> entry : requestBody.getContent().entrySet()) {
isRequestBodySchemaSet |= entry.getValue() != null && entry.getValue().getSchema() != null;
}
}
RequestBody currentRequestBody = swaggerOperation.getRequestBody();
if (currentRequestBody != null) {
swaggerOperation.setRequestBody(mergeRequestBody(currentRequestBody, requestBody));
Expand All @@ -454,14 +461,15 @@ public void visitMethod(MethodElement element, VisitorContext context) {
List<TypedElement> extraBodyParameters = new ArrayList<>();
for (io.swagger.v3.oas.models.Operation operation : swaggerOperations.values()) {
processParameters(element, context, openAPI, operation, javadocDescription, permitsRequestBody, pathVariables, consumesMediaTypes, extraBodyParameters, httpMethod, matchTemplates, pathItems);
processExtraBodyParameters(context, httpMethod, openAPI, operation, javadocDescription, consumesMediaTypes, extraBodyParameters);
processExtraBodyParameters(context, httpMethod, openAPI, operation, javadocDescription, isRequestBodySchemaSet, consumesMediaTypes, extraBodyParameters);
}
}
}

private void processExtraBodyParameters(VisitorContext context, HttpMethod httpMethod, OpenAPI openAPI,
io.swagger.v3.oas.models.Operation swaggerOperation,
JavadocDescription javadocDescription,
boolean isRequestBodySchemaSet,
List<MediaType> consumesMediaTypes,
List<TypedElement> extraBodyParameters) {
RequestBody requestBody = swaggerOperation.getRequestBody();
Expand Down Expand Up @@ -491,31 +499,39 @@ private void processExtraBodyParameters(VisitorContext context, HttpMethod httpM
mediaType.setSchema(schema);
}
if (schema.get$ref() != null) {
ComposedSchema composedSchema = new ComposedSchema();
Schema extraBodyParametersSchema = new Schema();
// Composition of existing + a new schema where extra body parameters are going to be added
composedSchema.addAllOfItem(schema);
composedSchema.addAllOfItem(extraBodyParametersSchema);
schema = extraBodyParametersSchema;
mediaType.setSchema(composedSchema);
if (isRequestBodySchemaSet) {
schema = openAPI.getComponents().getSchemas().get(schema.get$ref().substring(COMPONENTS_SCHEMAS_PREFIX.length()));
} else {
ComposedSchema composedSchema = new ComposedSchema();
Schema extraBodyParametersSchema = new Schema();
// Composition of existing + a new schema where extra body parameters are going to be added
composedSchema.addAllOfItem(schema);
composedSchema.addAllOfItem(extraBodyParametersSchema);
schema = extraBodyParametersSchema;
mediaType.setSchema(composedSchema);
}
}
for (TypedElement parameter : extraBodyParameters) {
processBodyParameter(context, openAPI, javadocDescription, MediaType.of(mediaTypeName), schema, parameter);
if (!isRequestBodySchemaSet) {
processBodyParameter(context, openAPI, javadocDescription, MediaType.of(mediaTypeName), schema, parameter);
}
if (mediaTypeName.equals(MediaType.MULTIPART_FORM_DATA)) {
for (String prop : (Set<String>) schema.getProperties().keySet()) {
Map<String, Encoding> encodings = mediaType.getEncoding();
if (encodings == null) {
encodings = new HashMap<>();
mediaType.setEncoding(encodings);
}
// if content type doesn't set by annotation,
// we can set application/octet-stream for file upload classes
Encoding encoding = encodings.get(prop);
if (encoding == null && isFileUpload(parameter.getType())) {
encoding = new Encoding();
encodings.put(prop, encoding);

encoding.setContentType(MediaType.APPLICATION_OCTET_STREAM);
if (CollectionUtils.isNotEmpty(schema.getProperties())) {
for (String prop : (Set<String>) schema.getProperties().keySet()) {
Map<String, Encoding> encodings = mediaType.getEncoding();
if (encodings == null) {
encodings = new HashMap<>();
mediaType.setEncoding(encodings);
}
// if content type doesn't set by annotation,
// we can set application/octet-stream for file upload classes
Encoding encoding = encodings.get(prop);
if (encoding == null && isFileUpload(parameter.getType())) {
encoding = new Encoding();
encodings.put(prop, encoding);

encoding.setContentType(MediaType.APPLICATION_OCTET_STREAM);
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,6 @@
import static io.micronaut.openapi.visitor.OpenApiApplicationVisitor.getConfigurationProperty;
import static io.micronaut.openapi.visitor.OpenApiApplicationVisitor.getExpandableProperties;
import static io.micronaut.openapi.visitor.OpenApiApplicationVisitor.resolvePlaceholders;
import static io.micronaut.openapi.visitor.SchemaUtils.EMPTY_SCHEMA;
import static io.micronaut.openapi.visitor.SchemaUtils.TYPE_OBJECT;
import static io.micronaut.openapi.visitor.Utils.resolveComponents;
import static java.util.stream.Collectors.toMap;
Expand All @@ -146,7 +145,6 @@
abstract class AbstractOpenApiVisitor {

private static final Lock VISITED_ELEMENTS_LOCK = new ReentrantLock();
private static final ComposedSchema EMPTY_COMPOSED_SCHEMA = new ComposedSchema();

/**
* Stores relations between schema names and class names.
Expand Down Expand Up @@ -1258,7 +1256,7 @@ protected Schema bindSchemaForElement(VisitorContext context, TypedElement eleme
notOnlyRef = true;
}

boolean addSchemaToBind = !schemaToBind.equals(EMPTY_SCHEMA);
boolean addSchemaToBind = !SchemaUtils.isEmptySchema(schemaToBind);

if (addSchemaToBind) {
if (TYPE_OBJECT.equals(originalSchema.getType())) {
Expand All @@ -1267,7 +1265,7 @@ protected Schema bindSchemaForElement(VisitorContext context, TypedElement eleme
}
originalSchema.setType(null);
}
if (!originalSchema.equals(EMPTY_SCHEMA)) {
if (!SchemaUtils.isEmptySchema(originalSchema)) {
composedSchema.addAllOfItem(originalSchema);
}
} else if (isNullable && CollectionUtils.isEmpty(composedSchema.getAllOf())) {
Expand All @@ -1283,7 +1281,7 @@ protected Schema bindSchemaForElement(VisitorContext context, TypedElement eleme
composedSchema.addAllOfItem(schemaToBind);
}

if (!composedSchema.equals(EMPTY_COMPOSED_SCHEMA)
if (!SchemaUtils.isEmptySchema(composedSchema)
&& ((CollectionUtils.isNotEmpty(composedSchema.getAllOf()) && composedSchema.getAllOf().size() > 1)
|| CollectionUtils.isNotEmpty(composedSchema.getOneOf())
|| CollectionUtils.isNotEmpty(composedSchema.getAnyOf())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,6 @@
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;

import static io.micronaut.openapi.visitor.SchemaUtils.EMPTY_COMPOSED_SCHEMA;
import static io.micronaut.openapi.visitor.SchemaUtils.EMPTY_SCHEMA;
import static io.micronaut.openapi.visitor.SchemaUtils.EMPTY_SIMPLE_SCHEMA;
import static io.micronaut.openapi.visitor.SchemaUtils.TYPE_OBJECT;
import static io.swagger.v3.oas.models.Components.COMPONENTS_SCHEMAS_REF;
Expand Down Expand Up @@ -1450,7 +1448,7 @@ private Schema normalizeSchema(Schema schema) {
}
boolean isSameType = allOfSchema.getType() == null || allOfSchema.getType().equals(type);

if (schema.equals(EMPTY_SCHEMA) || schema.equals(EMPTY_COMPOSED_SCHEMA)
if (SchemaUtils.isEmptySchema(schema)
&& (serializedDefaultValue == null || serializedDefaultValue.equals(serializedAllOfDefaultValue))
&& (type == null || allOfSchema.getType() == null || allOfSchema.getType().equals(type))) {
normalizedSchema = allOfSchema;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,33 @@
*/
package io.micronaut.openapi.visitor;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import io.micronaut.core.annotation.Internal;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.media.ArraySchema;
import io.swagger.v3.oas.models.media.BinarySchema;
import io.swagger.v3.oas.models.media.BooleanSchema;
import io.swagger.v3.oas.models.media.ByteArraySchema;
import io.swagger.v3.oas.models.media.ComposedSchema;
import io.swagger.v3.oas.models.media.DateSchema;
import io.swagger.v3.oas.models.media.DateTimeSchema;
import io.swagger.v3.oas.models.media.EmailSchema;
import io.swagger.v3.oas.models.media.FileSchema;
import io.swagger.v3.oas.models.media.IntegerSchema;
import io.swagger.v3.oas.models.media.JsonSchema;
import io.swagger.v3.oas.models.media.MapSchema;
import io.swagger.v3.oas.models.media.NumberSchema;
import io.swagger.v3.oas.models.media.ObjectSchema;
import io.swagger.v3.oas.models.media.PasswordSchema;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.media.StringSchema;
import io.swagger.v3.oas.models.media.UUIDSchema;

import static io.micronaut.openapi.visitor.Utils.resolveComponents;
import static io.swagger.v3.oas.models.Components.COMPONENTS_SCHEMAS_REF;
Expand All @@ -36,14 +54,64 @@
@Internal
public final class SchemaUtils {

public static final String COMPONENTS_CALLBACKS_PREFIX = "#/components/callbacks/";
public static final String COMPONENTS_SCHEMAS_PREFIX = "#/components/schemas/";

public static final Schema<?> EMPTY_SCHEMA = new Schema<>();
public static final Schema<?> EMPTY_SIMPLE_SCHEMA = new SimpleSchema();
public static final Schema<?> EMPTY_ARRAY_SCHEMA = new ArraySchema();
public static final Schema<?> EMPTY_BINARY_SCHEMA = new BinarySchema();
public static final Schema<?> EMPTY_BOOLEAN_SCHEMA = new BooleanSchema();
public static final Schema<?> EMPTY_BYTE_ARRAY_SCHEMA = new ByteArraySchema();
public static final Schema<?> EMPTY_COMPOSED_SCHEMA = new ComposedSchema();
public static final Schema<?> EMPTY_DATE_SCHEMA = new DateSchema();
public static final Schema<?> EMPTY_DATE_TIME_SCHEMA = new DateTimeSchema();
public static final Schema<?> EMPTY_EMAIL_SCHEMA = new EmailSchema();
public static final Schema<?> EMPTY_FILE_SCHEMA = new FileSchema();
public static final Schema<?> EMPTY_INTEGER_SCHEMA = new IntegerSchema();
public static final Schema<?> EMPTY_JSON_SCHEMA = new JsonSchema();
public static final Schema<?> EMPTY_MAP_SCHEMA = new MapSchema();
public static final Schema<?> EMPTY_NUMBER_SCHEMA = new NumberSchema();
public static final Schema<?> EMPTY_OBJECT_SCHEMA = new ObjectSchema();
public static final Schema<?> EMPTY_PASSWORD_SCHEMA = new PasswordSchema();
public static final Schema<?> EMPTY_STRING_SCHEMA = new StringSchema();
public static final Schema<?> EMPTY_UUID_SCHEMA = new UUIDSchema();
public static final Schema<?> EMPTY_SIMPLE_SCHEMA = new SimpleSchema();

private static final List<Schema<?>> ALL_EMPTY_SCHEMAS;
static {
List<Schema<?>> schemas = new ArrayList<>();
schemas.add(EMPTY_SCHEMA);
schemas.add(EMPTY_ARRAY_SCHEMA);
schemas.add(EMPTY_BINARY_SCHEMA);
schemas.add(EMPTY_BOOLEAN_SCHEMA);
schemas.add(EMPTY_BYTE_ARRAY_SCHEMA);
schemas.add(EMPTY_COMPOSED_SCHEMA);
schemas.add(EMPTY_DATE_SCHEMA);
schemas.add(EMPTY_DATE_TIME_SCHEMA);
schemas.add(EMPTY_EMAIL_SCHEMA);
schemas.add(EMPTY_FILE_SCHEMA);
schemas.add(EMPTY_INTEGER_SCHEMA);
schemas.add(EMPTY_JSON_SCHEMA);
schemas.add(EMPTY_MAP_SCHEMA);
schemas.add(EMPTY_NUMBER_SCHEMA);
schemas.add(EMPTY_OBJECT_SCHEMA);
schemas.add(EMPTY_PASSWORD_SCHEMA);
schemas.add(EMPTY_STRING_SCHEMA);
schemas.add(EMPTY_UUID_SCHEMA);
schemas.add(EMPTY_SIMPLE_SCHEMA);

ALL_EMPTY_SCHEMAS = Collections.unmodifiableList(schemas);
}

public static final String TYPE_OBJECT = "object";

private SchemaUtils() {
}

public static boolean isEmptySchema(Schema<?> schema) {
return ALL_EMPTY_SCHEMAS.contains(schema);
}

public static Map<String, Schema> resolveSchemas(OpenAPI openAPI) {
Components components = resolveComponents(openAPI);
Map<String, Schema> schemas = components.getSchemas();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -807,4 +807,104 @@ class MyBean {}
operation.requestBody.content."multipart/form-data".encoding."template".contentType == "application/octet-stream"
operation.requestBody.content."multipart/form-data".encoding."parameters".contentType == "application/json"
}

void "test build OpenAPI multipart form data with custom schema"() {

when:
buildBeanDefinition('test.MyBean', '''
package test;
import java.util.HashMap;
import javax.validation.constraints.NotNull;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Post;
import io.micronaut.http.multipart.CompletedFileUpload;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Encoding;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import jakarta.inject.Singleton;
@Controller("/path/{input}")
class OpenApiController {
/**
* Operation description.
*
* @param template Template description
* @param parameters Parameters description
*/
@RequestBody(
description = "Body description",
content = @Content(
schema = @Schema(implementation = UploadItem.class),
encoding = @Encoding(name = "parameters", contentType = MediaType.APPLICATION_JSON)))
@Post(consumes = MediaType.MULTIPART_FORM_DATA)
public String print(String input,
@NotNull CompletedFileUpload template,
String parameters) {
return null;
}
}
class UploadItem {
/**
* Upload item template description
*/
@Schema(type = "string", format = "binary")
@NotNull
public String template;
@NotNull
public Parameters parameters;
}
class Parameters extends HashMap<String, Object> {
}
@Singleton
class MyBean {}
''')
then: "the state is correct"
Utils.testReference != null

when: "The OpenAPI is retrieved"
OpenAPI openAPI = Utils.testReference
Operation operation = openAPI.paths.get("/path/{input}").post

then:
operation

operation.parameters.size() == 1
operation.parameters.get(0).name == 'input'
operation.parameters.get(0).in == 'path'
operation.parameters.get(0).required
operation.parameters.get(0).schema
operation.parameters.get(0).schema.type == 'string'

operation.requestBody
operation.requestBody.description == "Body description"
operation.requestBody.content
operation.requestBody.content.size() == 1
operation.requestBody.content."multipart/form-data"
operation.requestBody.content."multipart/form-data".encoding."template".contentType == "application/octet-stream"
operation.requestBody.content."multipart/form-data".encoding."parameters".contentType == "application/json"
operation.requestBody.content."multipart/form-data".schema.$ref == '#/components/schemas/UploadItem'

openAPI.components.schemas.UploadItem.required.size() == 2
openAPI.components.schemas.UploadItem.required.contains('template')
openAPI.components.schemas.UploadItem.required.contains('parameters')
openAPI.components.schemas.UploadItem.properties.size() == 2
openAPI.components.schemas.UploadItem.properties.'template'.type == 'string'
openAPI.components.schemas.UploadItem.properties.'template'.format == 'binary'
openAPI.components.schemas.UploadItem.properties.'template'.description == 'Upload item template description'
openAPI.components.schemas.UploadItem.properties.'parameters'.type == 'object'
}
}

0 comments on commit 2629523

Please sign in to comment.