diff --git a/openapi/src/main/java/io/micronaut/openapi/visitor/AbstractOpenApiEndpointVisitor.java b/openapi/src/main/java/io/micronaut/openapi/visitor/AbstractOpenApiEndpointVisitor.java index a1380883f0..2100c3eb80 100644 --- a/openapi/src/main/java/io/micronaut/openapi/visitor/AbstractOpenApiEndpointVisitor.java +++ b/openapi/src/main/java/io/micronaut/openapi/visitor/AbstractOpenApiEndpointVisitor.java @@ -35,6 +35,7 @@ import java.util.stream.Collectors; import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.beans.BeanMap; import io.micronaut.core.bind.annotation.Bindable; import io.micronaut.core.naming.NameUtils; @@ -113,6 +114,7 @@ import io.swagger.v3.oas.models.servers.Server; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonView; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; @@ -120,6 +122,7 @@ import static io.micronaut.openapi.visitor.ElementUtils.isNullable; import static io.micronaut.openapi.visitor.OpenApiApplicationVisitor.getGroupsPropertiesMap; import static io.micronaut.openapi.visitor.OpenApiApplicationVisitor.getSecurityProperties; +import static io.micronaut.openapi.visitor.OpenApiApplicationVisitor.isJsonViewEnabled; 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; @@ -235,7 +238,7 @@ private void processTags(ClassElement element, VisitorContext context) { private void processExternalDocs(ClassElement element, VisitorContext context) { final Optional> externalDocsAnn = element.findAnnotation(ExternalDocumentation.class); classExternalDocs = externalDocsAnn - .flatMap(o -> toValue(o.getValues(), context, io.swagger.v3.oas.models.ExternalDocumentation.class)) + .flatMap(o -> toValue(o.getValues(), context, io.swagger.v3.oas.models.ExternalDocumentation.class, null)) .orElse(null); } @@ -381,11 +384,24 @@ public void visitMethod(MethodElement element, VisitorContext context) { List consumesMediaTypes = consumesMediaTypes(element); List producesMediaTypes = producesMediaTypes(element); + ClassElement jsonViewClass = null; + if (isJsonViewEnabled(context)) { + AnnotationValue jsonViewAnnotation = element.findAnnotation(JsonView.class).orElse(null); + if (jsonViewAnnotation == null) { + jsonViewAnnotation = element.getOwningType().findAnnotation(JsonView.class).orElse(null); + } + if (jsonViewAnnotation != null) { + String jsonViewClassName = jsonViewAnnotation.stringValue().orElse(null); + if (jsonViewClassName != null) { + jsonViewClass = context.getClassElement(jsonViewClassName).orElse(null); + } + } + } + for (Map.Entry> pathItemEntry : pathItemsMap.entrySet()) { List pathItems = pathItemEntry.getValue(); - Map swaggerOperations = readOperations(pathItemEntry.getKey(), httpMethod, pathItems, element, context); - + Map swaggerOperations = readOperations(pathItemEntry.getKey(), httpMethod, pathItems, element, context, jsonViewClass); for (Map.Entry operationEntry : swaggerOperations.entrySet()) { io.swagger.v3.oas.models.Operation swaggerOperation = operationEntry.getValue(); @@ -401,11 +417,11 @@ public void visitMethod(MethodElement element, VisitorContext context) { readSecurityRequirements(element, pathItemEntry.getKey(), swaggerOperation, context); - readApiResponses(element, context, swaggerOperation); + readApiResponses(element, context, swaggerOperation, jsonViewClass); readServers(element, context, swaggerOperation); - readCallbacks(element, context, swaggerOperation); + readCallbacks(element, context, swaggerOperation, jsonViewClass); javadocDescription = getMethodDescription(element, swaggerOperation); @@ -413,7 +429,7 @@ public void visitMethod(MethodElement element, VisitorContext context) { swaggerOperation.setDeprecated(true); } - readResponse(element, context, openAPI, swaggerOperation, javadocDescription); + readResponse(element, context, openAPI, swaggerOperation, javadocDescription, jsonViewClass); boolean isRequestBodySchemaSet = false; @@ -693,9 +709,11 @@ private void processParameter(VisitorContext context, OpenAPI openAPI, io.swagger.v3.oas.models.media.MediaType mediaType = entry.getValue(); - Schema propertySchema = bindSchemaForElement(context, parameter, parameterType, mediaType.getSchema()); + Schema propertySchema = bindSchemaForElement(context, parameter, parameterType, mediaType.getSchema(), null); + + AnnotationValue bodyAnn = parameter.getAnnotation(Body.class); - String bodyAnnValue = parameter.getAnnotation(Body.class).getValue(String.class).orElse(null); + String bodyAnnValue = bodyAnn != null ? bodyAnn.getValue(String.class).orElse(null) : null; if (StringUtils.isNotEmpty(bodyAnnValue)) { Schema wrapperSchema = new Schema(); wrapperSchema.setType(TYPE_OBJECT); @@ -728,7 +746,7 @@ private void processParameter(VisitorContext context, OpenAPI openAPI, } if (newParameter.getExplode() != null && newParameter.getExplode() && "query".equals(newParameter.getIn()) && !parameterType.isIterable()) { - Schema explodedSchema = resolveSchema(openAPI, parameter, parameterType, context, consumesMediaTypes, null, null); + Schema explodedSchema = resolveSchema(openAPI, parameter, parameterType, context, consumesMediaTypes, null, null, null); if (explodedSchema != null) { if (openAPI.getComponents() != null && openAPI.getComponents().getSchemas() != null && StringUtils.isNotEmpty(explodedSchema.get$ref())) { explodedSchema = openAPI.getComponents().getSchemas().get(explodedSchema.get$ref().substring(Components.COMPONENTS_SCHEMAS_REF.length())); @@ -766,11 +784,11 @@ private void processParameter(VisitorContext context, OpenAPI openAPI, Schema schema = newParameter.getSchema(); if (schema == null) { - schema = resolveSchema(openAPI, parameter, parameterType, context, consumesMediaTypes, null, null); + schema = resolveSchema(openAPI, parameter, parameterType, context, consumesMediaTypes, null, null, null); } if (schema != null) { - schema = bindSchemaForElement(context, parameter, parameterType, schema); + schema = bindSchemaForElement(context, parameter, parameterType, schema, null); newParameter.setSchema(schema); } } @@ -791,8 +809,19 @@ private void addSwaggerParamater(Parameter newParameter, List swagger private void processBodyParameter(VisitorContext context, OpenAPI openAPI, JavadocDescription javadocDescription, MediaType mediaType, Schema schema, TypedElement parameter) { - Schema propertySchema = resolveSchema(openAPI, parameter, parameter.getType(), context, - Collections.singletonList(mediaType), null, null); + + ClassElement jsonViewClass = null; + if (isJsonViewEnabled(context)) { + AnnotationValue jsonViewAnnotation = parameter.findAnnotation(JsonView.class).orElse(null); + if (jsonViewAnnotation != null) { + String jsonViewClassName = jsonViewAnnotation.stringValue().orElse(null); + if (jsonViewClassName != null) { + jsonViewClass = context.getClassElement(jsonViewClassName).orElse(null); + } + } + } + + Schema propertySchema = resolveSchema(openAPI, parameter, parameter.getType(), context, Collections.singletonList(mediaType), jsonViewClass, null, null); if (propertySchema != null) { Optional description = parameter.getValue(io.swagger.v3.oas.annotations.Parameter.class, "description", String.class); @@ -926,7 +955,7 @@ private Parameter processMethodParameterAnnotation(VisitorContext context, io.sw return null; } - Map paramValues = toValueMap(paramAnn.getValues(), context); + Map paramValues = toValueMap(paramAnn.getValues(), context, null); Utils.normalizeEnumValues(paramValues, Collections.singletonMap("in", ParameterIn.class)); if (parameter.isAnnotationPresent(Header.class)) { paramValues.put("in", ParameterIn.HEADER.toString()); @@ -995,7 +1024,7 @@ private Parameter processMethodParameterAnnotation(VisitorContext context, io.sw final AnnotationValue schemaAnn = paramAnn.get("schema", AnnotationValue.class) .orElse(null); if (schemaAnn != null) { - bindSchemaAnnotationValue(context, parameter, parameterSchema, schemaAnn); + bindSchemaAnnotationValue(context, parameter, parameterSchema, schemaAnn, null); } } } @@ -1013,6 +1042,18 @@ private void processBody(VisitorContext context, OpenAPI openAPI, io.swagger.v3.oas.models.Operation swaggerOperation, JavadocDescription javadocDescription, boolean permitsRequestBody, List consumesMediaTypes, TypedElement parameter, ClassElement parameterType) { + + ClassElement jsonViewClass = null; + if (isJsonViewEnabled(context)) { + AnnotationValue jsonViewAnnotation = parameter.findAnnotation(JsonView.class).orElse(null); + if (jsonViewAnnotation != null) { + String jsonViewClassName = jsonViewAnnotation.stringValue().orElse(null); + if (jsonViewClassName != null) { + jsonViewClass = context.getClassElement(jsonViewClassName).orElse(null); + } + } + } + if (!permitsRequestBody) { return; } @@ -1031,7 +1072,7 @@ private void processBody(VisitorContext context, OpenAPI openAPI, requestBody.setRequired(true); } - final Content content = buildContent(parameter, parameterType, consumesMediaTypes, openAPI, context); + final Content content = buildContent(parameter, parameterType, consumesMediaTypes, openAPI, context, jsonViewClass); if (requestBody.getContent() == null) { requestBody.setContent(content); } else { @@ -1080,13 +1121,13 @@ private void processRequestBean(VisitorContext context, OpenAPI openAPI, } private void readResponse(MethodElement element, VisitorContext context, OpenAPI openAPI, - io.swagger.v3.oas.models.Operation swaggerOperation, JavadocDescription javadocDescription) { + io.swagger.v3.oas.models.Operation swaggerOperation, JavadocDescription javadocDescription, @Nullable ClassElement jsonViewClass) { boolean withMethodResponses = element.hasDeclaredAnnotation(io.swagger.v3.oas.annotations.responses.ApiResponse.class) || element.hasDeclaredAnnotation(io.swagger.v3.oas.annotations.responses.ApiResponse.class); HttpStatus methodResponseStatus = element.enumValue(Status.class, HttpStatus.class).orElse(HttpStatus.OK); - String responseCode = String.valueOf(methodResponseStatus.getCode()); + String responseCode = Integer.toString(methodResponseStatus.getCode()); ApiResponses responses = swaggerOperation.getResponses(); ApiResponse response = null; @@ -1108,22 +1149,22 @@ private void readResponse(MethodElement element, VisitorContext context, OpenAPI } else { response.setDescription(javadocDescription.getReturnDescription()); } - addResponseContent(element, context, openAPI, response); + addResponseContent(element, context, openAPI, response, jsonViewClass); responses.put(responseCode, response); } else if (response != null && response.getContent() == null) { - addResponseContent(element, context, openAPI, response); + addResponseContent(element, context, openAPI, response, jsonViewClass); } } - private void addResponseContent(MethodElement element, VisitorContext context, OpenAPI openAPI, ApiResponse response) { + private void addResponseContent(MethodElement element, VisitorContext context, OpenAPI openAPI, ApiResponse response, @Nullable ClassElement jsonViewClass) { ClassElement returnType = returnType(element, context); if (returnType != null && !returnType.getCanonicalName().equals(Void.class.getName())) { List producesMediaTypes = producesMediaTypes(element); Content content; if (producesMediaTypes.isEmpty()) { - content = buildContent(element, returnType, DEFAULT_MEDIA_TYPES, openAPI, context); + content = buildContent(element, returnType, DEFAULT_MEDIA_TYPES, openAPI, context, jsonViewClass); } else { - content = buildContent(element, returnType, producesMediaTypes, openAPI, context); + content = buildContent(element, returnType, producesMediaTypes, openAPI, context, jsonViewClass); } response.setContent(content); } @@ -1179,12 +1220,13 @@ private JavadocDescription getMethodDescription(MethodElement element, return javadocDescription; } - private Map readOperations(String path, HttpMethod httpMethod, List pathItems, MethodElement element, VisitorContext context) { + private Map readOperations(String path, HttpMethod httpMethod, List pathItems, MethodElement element, VisitorContext context, @Nullable ClassElement jsonViewClass) { Map swaggerOperations = new HashMap<>(pathItems.size()); final Optional> operationAnnotation = element.findAnnotation(Operation.class); + for (PathItem pathItem : pathItems) { io.swagger.v3.oas.models.Operation swaggerOperation = operationAnnotation - .flatMap(o -> toValue(o.getValues(), context, io.swagger.v3.oas.models.Operation.class)) + .flatMap(o -> toValue(o.getValues(), context, io.swagger.v3.oas.models.Operation.class, jsonViewClass)) .orElse(new io.swagger.v3.oas.models.Operation()); if (CollectionUtils.isNotEmpty(swaggerOperation.getParameters())) { @@ -1352,7 +1394,7 @@ private Parameter.StyleEnum paramStyle(ParameterStyle paramAnnStyle) { private io.swagger.v3.oas.models.ExternalDocumentation readExternalDocs(MethodElement element, VisitorContext context) { final Optional> externalDocsAnn = element.findAnnotation(ExternalDocumentation.class); io.swagger.v3.oas.models.ExternalDocumentation externalDocs = externalDocsAnn - .flatMap(o -> toValue(o.getValues(), context, io.swagger.v3.oas.models.ExternalDocumentation.class)) + .flatMap(o -> toValue(o.getValues(), context, io.swagger.v3.oas.models.ExternalDocumentation.class, null)) .orElse(null); return externalDocs; @@ -1557,15 +1599,16 @@ private boolean isSingleResponseType(ClassElement returnType) { && isResponseType(returnType.getFirstTypeArgument().get()); } - private void readApiResponses(MethodElement element, VisitorContext context, io.swagger.v3.oas.models.Operation swaggerOperation) { + private void readApiResponses(MethodElement element, VisitorContext context, io.swagger.v3.oas.models.Operation swaggerOperation, @Nullable ClassElement jsonViewClass) { List> methodResponseAnnotations = element.getAnnotationValuesByType(io.swagger.v3.oas.annotations.responses.ApiResponse.class); - processResponses(swaggerOperation, methodResponseAnnotations, element, context); + processResponses(swaggerOperation, methodResponseAnnotations, element, context, jsonViewClass); List> classResponseAnnotations = element.getDeclaringType().getAnnotationValuesByType(io.swagger.v3.oas.annotations.responses.ApiResponse.class); - processResponses(swaggerOperation, classResponseAnnotations, element, context); + processResponses(swaggerOperation, classResponseAnnotations, element, context, jsonViewClass); } - private void processResponses(io.swagger.v3.oas.models.Operation operation, List> responseAnnotations, MethodElement element, VisitorContext context) { + private void processResponses(io.swagger.v3.oas.models.Operation operation, List> responseAnnotations, + MethodElement element, VisitorContext context, @Nullable ClassElement jsonViewClass) { ApiResponses apiResponses = operation.getResponses(); if (apiResponses == null) { apiResponses = new ApiResponses(); @@ -1577,11 +1620,11 @@ private void processResponses(io.swagger.v3.oas.models.Operation operation, List if (apiResponses.containsKey(responseCode)) { continue; } - Optional newResponse = toValue(response.getValues(), context, ApiResponse.class); + Optional newResponse = toValue(response.getValues(), context, ApiResponse.class, jsonViewClass); if (newResponse.isPresent()) { ApiResponse newApiResponse = newResponse.get(); if (response.booleanValue("useReturnTypeSchema").orElse(false) && element != null) { - addResponseContent(element, context, Utils.resolveOpenApi(context), newApiResponse); + addResponseContent(element, context, Utils.resolveOpenApi(context), newApiResponse, jsonViewClass); } else { List producesMediaTypes = producesMediaTypes(element); @@ -1613,6 +1656,13 @@ private void processResponses(io.swagger.v3.oas.models.Operation operation, List newApiResponse.setContent(contentFromProduces); } } + try { + if (StringUtils.isEmpty(newApiResponse.getDescription())) { + newApiResponse.setDescription(responseCode.equals("default") ? "OK response" : HttpStatus.valueOf(Integer.parseInt(responseCode)).getReason()); + } + } catch (Exception e) { + newApiResponse.setDescription("Response " + responseCode); + } apiResponses.put(responseCode, newApiResponse); } } @@ -1639,7 +1689,18 @@ private Pair readSwaggerRequestBody(Element element, List< } } - RequestBody requestBody = toValue(requestBodyAnnValue.getValues(), context, RequestBody.class).orElse(null); + ClassElement jsonViewClass = null; + if (isJsonViewEnabled(context) && element instanceof ParameterElement) { + AnnotationValue jsonViewAnnotation = element.findAnnotation(JsonView.class).orElse(null); + if (jsonViewAnnotation != null) { + String jsonViewClassName = jsonViewAnnotation.stringValue().orElse(null); + if (jsonViewClassName != null) { + jsonViewClass = context.getClassElement(jsonViewClassName).orElse(null); + } + } + } + + RequestBody requestBody = toValue(requestBodyAnnValue.getValues(), context, RequestBody.class, jsonViewClass).orElse(null); // if media type doesn't set in swagger annotation, check micronaut annotation if (content != null && !content.stringValue("mediaType").isPresent() @@ -1663,7 +1724,7 @@ private void readServers(MethodElement element, VisitorContext context, io.swagg } private void readCallbacks(MethodElement element, VisitorContext context, - io.swagger.v3.oas.models.Operation swaggerOperation) { + io.swagger.v3.oas.models.Operation swaggerOperation, @Nullable ClassElement jsonViewClass) { AnnotationValue callbacksAnnotation = element.getAnnotation(Callbacks.class); List> callbackAnnotations; if (callbacksAnnotation != null) { @@ -1688,7 +1749,7 @@ private void readCallbacks(MethodElement element, VisitorContext context, } final Optional expr = callbackAnn.stringValue("callbackUrlExpression"); if (expr.isPresent()) { - processUrlCallbackExpression(context, swaggerOperation, callbackAnn, callbackName, expr.get()); + processUrlCallbackExpression(context, swaggerOperation, callbackAnn, callbackName, expr.get(), jsonViewClass); } else { processCallbackReference(context, swaggerOperation, callbackName, null); } @@ -1710,7 +1771,7 @@ private void processCallbackReference(VisitorContext context, io.swagger.v3.oas. private void processUrlCallbackExpression(VisitorContext context, io.swagger.v3.oas.models.Operation swaggerOperation, AnnotationValue callbackAnn, - String callbackName, final String callbackUrl) { + String callbackName, final String callbackUrl, @Nullable ClassElement jsonViewClass) { final List> operations = callbackAnn.getAnnotations("operation", Operation.class); if (CollectionUtils.isEmpty(operations)) { Map callbacks = initCallbacks( @@ -1722,7 +1783,7 @@ private void processUrlCallbackExpression(VisitorContext context, final PathItem pathItem = new PathItem(); for (AnnotationValue operation : operations) { final Optional operationMethod = operation.get("method", HttpMethod.class); - operationMethod.ifPresent(httpMethod -> toValue(operation.getValues(), context, io.swagger.v3.oas.models.Operation.class) + operationMethod.ifPresent(httpMethod -> toValue(operation.getValues(), context, io.swagger.v3.oas.models.Operation.class, jsonViewClass) .ifPresent(op -> setOperationOnPathItem(pathItem, httpMethod, op))); } Map callbacks = initCallbacks( @@ -1930,17 +1991,17 @@ private List readTags(ClassElement element, V final List readTags(List> annotations, VisitorContext context) { return annotations.stream() - .map(av -> toValue(av.getValues(), context, io.swagger.v3.oas.models.tags.Tag.class)) + .map(av -> toValue(av.getValues(), context, io.swagger.v3.oas.models.tags.Tag.class, null)) .filter(Optional::isPresent) .map(Optional::get) .collect(Collectors.toList()); } - private Content buildContent(Element definingElement, ClassElement type, List mediaTypes, OpenAPI openAPI, VisitorContext context) { + private Content buildContent(Element definingElement, ClassElement type, List mediaTypes, OpenAPI openAPI, VisitorContext context, @Nullable ClassElement jsonViewClass) { Content content = new Content(); mediaTypes.forEach(mediaType -> { io.swagger.v3.oas.models.media.MediaType mt = new io.swagger.v3.oas.models.media.MediaType(); - mt.setSchema(resolveSchema(openAPI, definingElement, type, context, Collections.singletonList(mediaType), null, null)); + mt.setSchema(resolveSchema(openAPI, definingElement, type, context, Collections.singletonList(mediaType), jsonViewClass, null, null)); content.addMediaType(mediaType.toString(), mt); }); return content; diff --git a/openapi/src/main/java/io/micronaut/openapi/visitor/AbstractOpenApiVisitor.java b/openapi/src/main/java/io/micronaut/openapi/visitor/AbstractOpenApiVisitor.java index 0ff5ab56d8..4785ad3f3f 100644 --- a/openapi/src/main/java/io/micronaut/openapi/visitor/AbstractOpenApiVisitor.java +++ b/openapi/src/main/java/io/micronaut/openapi/visitor/AbstractOpenApiVisitor.java @@ -65,6 +65,7 @@ import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.StringUtils; +import io.micronaut.http.HttpStatus; import io.micronaut.http.MediaType; import io.micronaut.http.uri.UriMatchTemplate; import io.micronaut.http.uri.UriMatchVariable; @@ -199,11 +200,12 @@ int visitedElements(VisitorContext context) { * * @param values The values * @param context The visitor context + * @param jsonViewClass Class from JsonView annotation * * @return The node */ - JsonNode toJson(Map values, VisitorContext context) { - Map newValues = toValueMap(values, context); + JsonNode toJson(Map values, VisitorContext context, @Nullable ClassElement jsonViewClass) { + Map newValues = toValueMap(values, context, jsonViewClass); return ConvertUtils.getJsonMapper().valueToTree(newValues); } @@ -214,11 +216,12 @@ JsonNode toJson(Map values, VisitorContext context) { * @param values The values * @param context The visitor context * @param type The class + * @param jsonViewClass Class from JsonView annotation * * @return The converted instance */ - Optional toValue(Map values, VisitorContext context, Class type) { - JsonNode node = toJson(values, context); + Optional toValue(Map values, VisitorContext context, Class type, @Nullable ClassElement jsonViewClass) { + JsonNode node = toJson(values, context, jsonViewClass); try { return Optional.ofNullable(ConvertUtils.treeToValue(node, type, context)); } catch (JsonProcessingException e) { @@ -390,10 +393,11 @@ private List addOptionalVars(List paths, String var, int level) * * @param values The values * @param context The visitor context + * @param jsonViewClass Class from JsonView annotation * * @return The map */ - protected Map toValueMap(Map values, VisitorContext context) { + protected Map toValueMap(Map values, VisitorContext context, @Nullable ClassElement jsonViewClass) { Map newValues = new HashMap<>(values.size()); for (Map.Entry entry : values.entrySet()) { CharSequence key = entry.getKey(); @@ -402,10 +406,10 @@ protected Map toValueMap(Map values, if (value instanceof AnnotationValue) { AnnotationValue av = (AnnotationValue) value; if (av.getAnnotationName().equals(io.swagger.v3.oas.annotations.media.ArraySchema.class.getName())) { - final Map valueMap = resolveArraySchemaAnnotationValues(context, av); + final Map valueMap = resolveArraySchemaAnnotationValues(context, av, jsonViewClass); newValues.put("schema", valueMap); } else { - final Map valueMap = resolveAnnotationValues(context, av); + final Map valueMap = resolveAnnotationValues(context, av, jsonViewClass); newValues.put(key, valueMap); } } else if (value instanceof AnnotationClassValue) { @@ -442,13 +446,13 @@ protected Map toValueMap(Map values, } newValues.put("extensions", extensions); } else if (Encoding.class.getName().equals(annotationName)) { - Map encodings = annotationValueArrayToSubmap(a, "name", context); + Map encodings = annotationValueArrayToSubmap(a, "name", context, null); newValues.put(key, encodings); } else if (Content.class.getName().equals(annotationName)) { - Map mediaTypes = annotationValueArrayToSubmap(a, "mediaType", context); + Map mediaTypes = annotationValueArrayToSubmap(a, "mediaType", context, jsonViewClass); newValues.put(key, mediaTypes); } else if (Link.class.getName().equals(annotationName) || Header.class.getName().equals(annotationName)) { - Map linksOrHeaders = annotationValueArrayToSubmap(a, "name", context); + Map linksOrHeaders = annotationValueArrayToSubmap(a, "name", context, jsonViewClass); for (Object linkOrHeader : linksOrHeaders.values()) { Map linkOrHeaderMap = (Map) linkOrHeader; if (linkOrHeaderMap.containsKey("ref")) { @@ -482,12 +486,21 @@ protected Map toValueMap(Map values, for (Object o : a) { AnnotationValue sv = (AnnotationValue) o; String name = sv.stringValue("responseCode").orElse("default"); - Map map = toValueMap(sv.getValues(), context); + Map map = toValueMap(sv.getValues(), context, jsonViewClass); if (map.containsKey("ref")) { Object ref = map.get("ref"); map.clear(); map.put("$ref", ref); } + + try { + if (!map.containsKey("description")) { + map.put("description", name.equals("default") ? "OK response" : HttpStatus.valueOf(Integer.parseInt(name)).getReason()); + } + } catch (Exception e) { + map.put("description", "Response " + name); + } + responses.put(name, map); } newValues.put(key, responses); @@ -496,7 +509,7 @@ protected Map toValueMap(Map values, for (Object o : a) { AnnotationValue sv = (AnnotationValue) o; String name = sv.stringValue("name").orElse("example"); - Map map = toValueMap(sv.getValues(), context); + Map map = toValueMap(sv.getValues(), context, null); if (map.containsKey("ref")) { Object ref = map.get("ref"); map.clear(); @@ -509,7 +522,7 @@ protected Map toValueMap(Map values, List> servers = new ArrayList<>(); for (Object o : a) { AnnotationValue sv = (AnnotationValue) o; - Map variables = new LinkedHashMap<>(toValueMap(sv.getValues(), context)); + Map variables = new LinkedHashMap<>(toValueMap(sv.getValues(), context, null)); servers.add(variables); } newValues.put(key, servers); @@ -519,7 +532,7 @@ protected Map toValueMap(Map values, AnnotationValue sv = (AnnotationValue) o; Optional n = sv.stringValue("name"); n.ifPresent(name -> { - Map map = toValueMap(sv.getValues(), context); + Map map = toValueMap(sv.getValues(), context, null); Object dv = map.get("defaultValue"); if (dv != null) { map.put("default", dv); @@ -536,7 +549,7 @@ protected Map toValueMap(Map values, final Map mappings = new HashMap<>(); for (Object o : a) { final AnnotationValue dv = (AnnotationValue) o; - final Map valueMap = resolveAnnotationValues(context, dv); + final Map valueMap = resolveAnnotationValues(context, dv, null); mappings.put(valueMap.get("value").toString(), valueMap.get("$ref").toString()); } final Map discriminatorMap = getDiscriminatorMap(newValues); @@ -545,15 +558,15 @@ protected Map toValueMap(Map values, } else { if (a.length == 1) { final AnnotationValue av = (AnnotationValue) a[0]; - final Map valueMap = resolveAnnotationValues(context, av); - newValues.put(key, toValueMap(valueMap, context)); + final Map valueMap = resolveAnnotationValues(context, av, jsonViewClass); + newValues.put(key, toValueMap(valueMap, context, jsonViewClass)); } else { List list = new ArrayList<>(); for (Object o : a) { if (o instanceof AnnotationValue) { final AnnotationValue av = (AnnotationValue) o; - final Map valueMap = resolveAnnotationValues(context, av); + final Map valueMap = resolveAnnotationValues(context, av, jsonViewClass); list.add(valueMap); } else { list.add(o); @@ -630,19 +643,20 @@ private Map getDiscriminatorMap(Map newVal return newValues.containsKey("discriminator") ? (Map) newValues.get("discriminator") : new HashMap<>(); } - private void processAnnotationValue(VisitorContext context, AnnotationValue annotationValue, Map arraySchemaMap, List filters, Class type) { + private void processAnnotationValue(VisitorContext context, AnnotationValue annotationValue, + Map arraySchemaMap, List filters, Class type, @Nullable ClassElement jsonViewClass) { Map values = annotationValue.getValues().entrySet().stream() .filter(entry -> filters == null || !filters.contains((String) entry.getKey())) .collect(toMap(e -> e.getKey().equals("requiredProperties") ? "required" : e.getKey(), Map.Entry::getValue)); - Optional schema = toValue(values, context, type); + Optional schema = toValue(values, context, type, jsonViewClass); schema.ifPresent(s -> schemaToValueMap(arraySchemaMap, s)); } - private Map resolveArraySchemaAnnotationValues(VisitorContext context, AnnotationValue av) { + private Map resolveArraySchemaAnnotationValues(VisitorContext context, AnnotationValue av, @Nullable ClassElement jsonViewClass) { final Map arraySchemaMap = new HashMap<>(10); // properties av.get("arraySchema", AnnotationValue.class).ifPresent(annotationValue -> - processAnnotationValue(context, (AnnotationValue) annotationValue, arraySchemaMap, Arrays.asList("ref", "implementation"), Schema.class) + processAnnotationValue(context, (AnnotationValue) annotationValue, arraySchemaMap, Arrays.asList("ref", "implementation"), Schema.class, null) ); // items av.get("schema", AnnotationValue.class).ifPresent(annotationValue -> { @@ -664,7 +678,7 @@ private Map resolveArraySchemaAnnotationValues(VisitorCont } if (classElement.isPresent()) { if (primitiveType == null) { - final ArraySchema schema = SchemaUtils.arraySchema(resolveSchema(null, classElement.get(), context, Collections.emptyList())); + final ArraySchema schema = SchemaUtils.arraySchema(resolveSchema(null, classElement.get(), context, Collections.emptyList(), jsonViewClass)); schemaToValueMap(arraySchemaMap, schema); } else { // For primitive type, just copy description field is present. @@ -674,17 +688,17 @@ private Map resolveArraySchemaAnnotationValues(VisitorCont schemaToValueMap(arraySchemaMap, schema); } } else { - arraySchemaMap.putAll(resolveAnnotationValues(context, annotationValue)); + arraySchemaMap.putAll(resolveAnnotationValues(context, annotationValue, jsonViewClass)); } }); // other properties (minItems,...) - processAnnotationValue(context, av, arraySchemaMap, Arrays.asList("schema", "arraySchema"), ArraySchema.class); + processAnnotationValue(context, av, arraySchemaMap, Arrays.asList("schema", "arraySchema"), ArraySchema.class, null); return arraySchemaMap; } - private Map resolveAnnotationValues(VisitorContext context, AnnotationValue av) { - final Map valueMap = toValueMap(av.getValues(), context); - bindSchemaIfNeccessary(context, av, valueMap); + private Map resolveAnnotationValues(VisitorContext context, AnnotationValue av, @Nullable ClassElement jsonViewClass) { + final Map valueMap = toValueMap(av.getValues(), context, jsonViewClass); + bindSchemaIfNeccessary(context, av, valueMap, jsonViewClass); final String annotationName = av.getAnnotationName(); if (Parameter.class.getName().equals(annotationName)) { Utils.normalizeEnumValues(valueMap, CollectionUtils.mapOf( @@ -719,12 +733,13 @@ private boolean isTypeNullable(ClassElement type) { * @param type The type element * @param context The context * @param mediaTypes An optional media type + * @param jsonViewClass Class from JsonView annotation * * @return The schema or null if it cannot be resolved */ @Nullable - protected Schema resolveSchema(@Nullable Element definingElement, ClassElement type, VisitorContext context, List mediaTypes) { - return resolveSchema(Utils.resolveOpenApi(context), definingElement, type, context, mediaTypes, null, null); + protected Schema resolveSchema(@Nullable Element definingElement, ClassElement type, VisitorContext context, List mediaTypes, @Nullable ClassElement jsonViewClass) { + return resolveSchema(Utils.resolveOpenApi(context), definingElement, type, context, mediaTypes, jsonViewClass, null, null); } /** @@ -737,12 +752,14 @@ protected Schema resolveSchema(@Nullable Element definingElement, ClassElement t * @param mediaTypes An optional media type * @param fieldJavadoc Field-level java doc * @param classJavadoc Class-level java doc + * @param jsonViewClass Class from JsonView annotation * * @return The schema or null if it cannot be resolved */ @Nullable protected Schema resolveSchema(OpenAPI openAPI, @Nullable Element definingElement, ClassElement type, VisitorContext context, - List mediaTypes, JavadocDescription fieldJavadoc, JavadocDescription classJavadoc) { + List mediaTypes, @Nullable ClassElement jsonViewClass, + JavadocDescription fieldJavadoc, JavadocDescription classJavadoc) { AnnotationValue schemaAnnotationValue = null; if (definingElement != null) { @@ -775,7 +792,7 @@ protected Schema resolveSchema(OpenAPI openAPI, @Nullable Element definingElemen Schema schema = null; if (type instanceof EnumElement) { - schema = getSchemaDefinition(openAPI, context, type, typeArgs, definingElement, mediaTypes); + schema = getSchemaDefinition(openAPI, context, type, typeArgs, definingElement, mediaTypes, jsonViewClass); } else { boolean isPublisher = false; @@ -838,18 +855,18 @@ protected Schema resolveSchema(OpenAPI openAPI, @Nullable Element definingElemen if (valueType.getName().equals(Object.class.getName())) { schema.setAdditionalProperties(true); } else { - schema.setAdditionalProperties(resolveSchema(openAPI, type, valueType, context, mediaTypes, null, classJavadoc)); + schema.setAdditionalProperties(resolveSchema(openAPI, type, valueType, context, mediaTypes, jsonViewClass, null, classJavadoc)); } } } else if (type.isIterable()) { if (type.isArray()) { - schema = resolveSchema(openAPI, type, type.fromArray(), context, mediaTypes, null, classJavadoc); + schema = resolveSchema(openAPI, type, type.fromArray(), context, mediaTypes, jsonViewClass, null, classJavadoc); if (schema != null) { schema = SchemaUtils.arraySchema(schema); } } else { if (componentType != null) { - schema = resolveSchema(openAPI, type, componentType, context, mediaTypes, null, classJavadoc); + schema = resolveSchema(openAPI, type, componentType, context, mediaTypes, jsonViewClass, null, classJavadoc); } else { schema = getPrimitiveType(Object.class.getName()); } @@ -857,7 +874,7 @@ protected Schema resolveSchema(OpenAPI openAPI, @Nullable Element definingElemen if (schema != null && fields.isEmpty()) { schema = SchemaUtils.arraySchema(schema); } else { - schema = getSchemaDefinition(openAPI, context, type, typeArgs, definingElement, mediaTypes); + schema = getSchemaDefinition(openAPI, context, type, typeArgs, definingElement, mediaTypes, jsonViewClass); } } } else if (ElementUtils.isReturnTypeFile(type)) { @@ -904,7 +921,7 @@ protected Schema resolveSchema(OpenAPI openAPI, @Nullable Element definingElemen } else if (type.getName().equals(Object.class.getName())) { schema = PrimitiveType.OBJECT.createProperty(); } else { - schema = getSchemaDefinition(openAPI, context, type, typeArgs, definingElement, mediaTypes); + schema = getSchemaDefinition(openAPI, context, type, typeArgs, definingElement, mediaTypes, jsonViewClass); } } @@ -949,7 +966,7 @@ private void handleUnwrapped(VisitorContext context, Element element, ClassEleme Map schemas = SchemaUtils.resolveSchemas(Utils.resolveOpenApi(context)); ClassElement customElementType = OpenApiApplicationVisitor.getCustomSchema(elementType.getName(), elementType.getTypeArguments(), context); String schemaName = element.stringValue(io.swagger.v3.oas.annotations.media.Schema.class, "name") - .orElse(computeDefaultSchemaName(null, customElementType != null ? customElementType : elementType, elementType.getTypeArguments(), context)); + .orElse(computeDefaultSchemaName(null, customElementType != null ? customElementType : elementType, elementType.getTypeArguments(), context, null)); Schema wrappedPropertySchema = schemas.get(schemaName); Map properties = wrappedPropertySchema.getProperties(); if (CollectionUtils.isEmpty(properties)) { @@ -984,7 +1001,8 @@ private void handleUnwrapped(VisitorContext context, Element element, ClassEleme * @param parentSchema The parent schema * @param propertySchema The property schema */ - protected void processSchemaProperty(VisitorContext context, TypedElement element, ClassElement elementType, @Nullable Element classElement, Schema parentSchema, Schema propertySchema) { + protected void processSchemaProperty(VisitorContext context, TypedElement element, ClassElement elementType, @Nullable Element classElement, + Schema parentSchema, Schema propertySchema) { if (propertySchema != null) { AnnotationValue uw = element.getAnnotation(JsonUnwrapped.class); if (uw != null && uw.booleanValue("enabled").orElse(Boolean.TRUE)) { @@ -1018,7 +1036,7 @@ protected void processSchemaProperty(VisitorContext context, TypedElement elemen required = true; } - propertySchema = bindSchemaForElement(context, element, elementType, propertySchema); + propertySchema = bindSchemaForElement(context, element, elementType, propertySchema, null); String propertyName = resolvePropertyName(element, classElement, propertySchema); propertySchema.setRequired(null); Schema propertySchemaFinal = propertySchema; @@ -1120,10 +1138,12 @@ private String resolvePropertyName(Element element, Element classElement, Schema * @param element The element * @param elementType The element type * @param schemaToBind The schema to bind + * @param jsonViewClass Class from JsonView annotation * * @return The bound schema */ - protected Schema bindSchemaForElement(VisitorContext context, TypedElement element, ClassElement elementType, Schema schemaToBind) { + protected Schema bindSchemaForElement(VisitorContext context, TypedElement element, ClassElement elementType, Schema schemaToBind, + @Nullable ClassElement jsonViewClass) { AnnotationValue schemaAnn = element.getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class); Schema originalSchema = schemaToBind; @@ -1136,7 +1156,7 @@ protected Schema bindSchemaForElement(VisitorContext context, TypedElement eleme if (originalSchema.get$ref() == null && schemaAnn != null) { // Apply @Schema annotation only if not $ref since for $ref schemas // we already populated values from right @Schema annotation in previous steps - schemaToBind = bindSchemaAnnotationValue(context, element, schemaToBind, schemaAnn); + schemaToBind = bindSchemaAnnotationValue(context, element, schemaToBind, schemaAnn, jsonViewClass); Optional schemaName = schemaAnn.stringValue("name"); if (schemaName.isPresent()) { schemaToBind.setName(schemaName.get()); @@ -1148,7 +1168,7 @@ protected Schema bindSchemaForElement(VisitorContext context, TypedElement eleme } AnnotationValue arraySchemaAnn = element.getAnnotation(io.swagger.v3.oas.annotations.media.ArraySchema.class); if (arraySchemaAnn != null) { - schemaToBind = bindArraySchemaAnnotationValue(context, element, schemaToBind, arraySchemaAnn); + schemaToBind = bindArraySchemaAnnotationValue(context, element, schemaToBind, arraySchemaAnn, jsonViewClass); Optional schemaName = arraySchemaAnn.stringValue("name"); if (schemaName.isPresent()) { schemaToBind.setName(schemaName.get()); @@ -1470,7 +1490,7 @@ void processShemaAnn(Schema schemaToBind, VisitorContext context, Element elemen AnnotationValue schemaExtDocs = (AnnotationValue) annValues.get("externalDocs"); ExternalDocumentation externalDocs = null; if (schemaExtDocs != null) { - externalDocs = toValue(schemaExtDocs.getValues(), context, ExternalDocumentation.class).orElse(null); + externalDocs = toValue(schemaExtDocs.getValues(), context, ExternalDocumentation.class, null).orElse(null); } if (externalDocs != null) { schemaToBind.setExternalDocs(externalDocs); @@ -1512,7 +1532,7 @@ void processShemaAnn(Schema schemaToBind, VisitorContext context, Element elemen OpenAPI openAPI = Utils.resolveOpenApi(context); Components components = resolveComponents(openAPI); - processClassValues(schemaToBind, annValues, Collections.emptyList(), context); + processClassValues(schemaToBind, annValues, Collections.emptyList(), context, null); String addProps = (String) annValues.get("additionalProperties"); if (StringUtils.isNotEmpty(addProps)) { @@ -1560,10 +1580,13 @@ private void setSchemaDocumentation(Element element, Schema schemaToBind) { * @param element The element * @param schemaToBind The schema to bind * @param schemaAnn The schema annotation + * @param jsonViewClass Class from JsonView annotation * * @return The bound schema */ - protected Schema bindSchemaAnnotationValue(VisitorContext context, Element element, Schema schemaToBind, AnnotationValue schemaAnn) { + protected Schema bindSchemaAnnotationValue(VisitorContext context, Element element, Schema schemaToBind, + AnnotationValue schemaAnn, + @Nullable ClassElement jsonViewClass) { ClassElement classElement = ((TypedElement) element).getType(); Pair typeAndFormat; @@ -1575,15 +1598,16 @@ protected Schema bindSchemaAnnotationValue(VisitorContext context, Element eleme typeAndFormat = ConvertUtils.getTypeAndFormatByClass(classElement.getName(), classElement.isArray()); } - JsonNode schemaJson = toJson(schemaAnn.getValues(), context); + JsonNode schemaJson = toJson(schemaAnn.getValues(), context, jsonViewClass); return doBindSchemaAnnotationValue(context, element, schemaToBind, schemaJson, schemaAnn.stringValue("type").orElse(typeAndFormat.getFirst()), schemaAnn.stringValue("format").orElse(typeAndFormat.getSecond()), - schemaAnn); + schemaAnn, jsonViewClass); } private Schema doBindSchemaAnnotationValue(VisitorContext context, Element element, Schema schemaToBind, - JsonNode schemaJson, String elType, String elFormat, AnnotationValue schemaAnn) { + JsonNode schemaJson, String elType, String elFormat, AnnotationValue schemaAnn, + @Nullable ClassElement jsonViewClass) { // need to set placeholders to set correct values to example field schemaJson = resolvePlaceholders(schemaJson, s -> expandProperties(s, getExpandableProperties(context), context)); @@ -1599,9 +1623,9 @@ private Schema doBindSchemaAnnotationValue(VisitorContext context, Element eleme defaultValue = schemaAnn.stringValue("defaultValue").orElse(null); allowableValues = schemaAnn.get("allowableValues", String[].class).orElse(null); Map annValues = schemaAnn.getValues(); - Map valueMap = toValueMap(annValues, context); - bindSchemaIfNeccessary(context, schemaAnn, valueMap); - processClassValues(schemaToBind, annValues, Collections.emptyList(), context); + Map valueMap = toValueMap(annValues, context, jsonViewClass); + bindSchemaIfNeccessary(context, schemaAnn, valueMap, jsonViewClass); + processClassValues(schemaToBind, annValues, Collections.emptyList(), context, jsonViewClass); } if (elType == null && element != null) { @@ -1643,11 +1667,14 @@ private Schema doBindSchemaAnnotationValue(VisitorContext context, Element eleme * @param element The element * @param schemaToBind The schema to bind * @param schemaAnn The schema annotation + * @param jsonViewClass Class from JsonView annotation * * @return The bound schema */ - protected Schema bindArraySchemaAnnotationValue(VisitorContext context, Element element, Schema schemaToBind, AnnotationValue schemaAnn) { - JsonNode schemaJson = toJson(schemaAnn.getValues(), context); + protected Schema bindArraySchemaAnnotationValue(VisitorContext context, Element element, Schema schemaToBind, + AnnotationValue schemaAnn, + @Nullable ClassElement jsonViewClass) { + JsonNode schemaJson = toJson(schemaAnn.getValues(), context, jsonViewClass); if (schemaJson.isObject()) { ObjectNode objNode = (ObjectNode) schemaJson; JsonNode arraySchema = objNode.remove("arraySchema"); @@ -1672,11 +1699,10 @@ protected Schema bindArraySchemaAnnotationValue(VisitorContext context, Element String elType = schemaJson.has("type") ? schemaJson.get("type").textValue() : null; String elFormat = schemaJson.has("format") ? schemaJson.get("format").textValue() : null; - // TODO !!!! - return doBindSchemaAnnotationValue(context, element, schemaToBind, schemaJson, elType, elFormat, null); + return doBindSchemaAnnotationValue(context, element, schemaToBind, schemaJson, elType, elFormat, null, jsonViewClass); } - private Map annotationValueArrayToSubmap(Object[] a, String classifier, VisitorContext context) { + private Map annotationValueArrayToSubmap(Object[] a, String classifier, VisitorContext context, @Nullable ClassElement jsonViewClass) { Map mediaTypes = new LinkedHashMap<>(); for (Object o : a) { AnnotationValue sv = (AnnotationValue) o; @@ -1685,7 +1711,7 @@ private Map annotationValueArrayToSubmap(Object[] a, String clas name = MediaType.APPLICATION_JSON; } if (name != null) { - Map map = toValueMap(sv.getValues(), context); + Map map = toValueMap(sv.getValues(), context, jsonViewClass); mediaTypes.put(name, map); } } @@ -1707,7 +1733,7 @@ private void schemaToValueMap(Map valueMap, Schema schema) } } - private void bindSchemaIfNeccessary(VisitorContext context, AnnotationValue av, Map valueMap) { + private void bindSchemaIfNeccessary(VisitorContext context, AnnotationValue av, Map valueMap, @Nullable ClassElement jsonViewClass) { final Optional impl = av.stringValue("implementation"); final Optional not = av.stringValue("not"); final Optional schema = av.stringValue("schema"); @@ -1727,30 +1753,30 @@ private void bindSchemaIfNeccessary(VisitorContext context, AnnotationValue a if (isSchema) { if (impl.isPresent()) { final String className = impl.get(); - bindSchemaForClassName(context, valueMap, className); + bindSchemaForClassName(context, valueMap, className, jsonViewClass); } if (not.isPresent()) { - final Schema schemaNot = resolveSchema(null, context.getClassElement(not.get()).get(), context, Collections.emptyList()); + final Schema schemaNot = resolveSchema(null, context.getClassElement(not.get()).get(), context, Collections.emptyList(), jsonViewClass); Map schemaMap = new HashMap<>(); schemaToValueMap(schemaMap, schemaNot); valueMap.put("not", schemaMap); } - anyOf.ifPresent(anyOfList -> bindSchemaForComposite(context, valueMap, anyOfList, "anyOf")); - oneOf.ifPresent(oneOfList -> bindSchemaForComposite(context, valueMap, oneOfList, "oneOf")); - allOf.ifPresent(allOfList -> bindSchemaForComposite(context, valueMap, allOfList, "allOf")); + anyOf.ifPresent(anyOfList -> bindSchemaForComposite(context, valueMap, anyOfList, "anyOf", jsonViewClass)); + oneOf.ifPresent(oneOfList -> bindSchemaForComposite(context, valueMap, oneOfList, "oneOf", jsonViewClass)); + allOf.ifPresent(allOfList -> bindSchemaForComposite(context, valueMap, allOfList, "allOf", jsonViewClass)); } if (DiscriminatorMapping.class.getName().equals(av.getAnnotationName()) && schema.isPresent()) { final String className = schema.get(); - bindSchemaForClassName(context, valueMap, className); + bindSchemaForClassName(context, valueMap, className, jsonViewClass); } } - private void bindSchemaForComposite(VisitorContext context, Map valueMap, String[] classNames, String key) { + private void bindSchemaForComposite(VisitorContext context, Map valueMap, String[] classNames, String key, @Nullable ClassElement jsonViewClass) { final List> namesToSchemas = Arrays.stream(classNames).map(className -> { final Optional classElement = context.getClassElement(className); Map schemaMap = new HashMap<>(); if (classElement.isPresent()) { - final Schema schema = resolveSchema(null, classElement.get(), context, Collections.emptyList()); + final Schema schema = resolveSchema(null, classElement.get(), context, Collections.emptyList(), jsonViewClass); schemaToValueMap(schemaMap, schema); } return schemaMap; @@ -1758,10 +1784,10 @@ private void bindSchemaForComposite(VisitorContext context, Map valueMap, String className) { + private void bindSchemaForClassName(VisitorContext context, Map valueMap, String className, @Nullable ClassElement jsonViewClass) { final Optional classElement = context.getClassElement(className); if (classElement.isPresent()) { - final Schema schema = resolveSchema(null, classElement.get(), context, Collections.emptyList()); + final Schema schema = resolveSchema(null, classElement.get(), context, Collections.emptyList(), jsonViewClass); schemaToValueMap(valueMap, schema); } } @@ -1785,13 +1811,14 @@ private void checkAllOf(Schema composedSchema) { composedSchema.addAllOfItem(propSchema); } - private Schema getSchemaDefinition( - OpenAPI openAPI, - VisitorContext context, - ClassElement type, - Map typeArgs, - @Nullable Element definingElement, - List mediaTypes) { + private Schema getSchemaDefinition(OpenAPI openAPI, + VisitorContext context, + ClassElement type, + Map typeArgs, + @Nullable Element definingElement, + List mediaTypes, + @Nullable ClassElement jsonViewClass + ) { AnnotationValue schemaValue = definingElement == null ? null : definingElement.getDeclaredAnnotation(io.swagger.v3.oas.annotations.media.Schema.class); if (schemaValue == null) { schemaValue = type.getDeclaredAnnotation(io.swagger.v3.oas.annotations.media.Schema.class); @@ -1807,7 +1834,7 @@ private Schema getSchemaDefinition( primitiveType = null; } if (primitiveType == null) { - String schemaName = computeDefaultSchemaName(definingElement, type, typeArgs, context); + String schemaName = computeDefaultSchemaName(definingElement, type, typeArgs, context, jsonViewClass); schema = schemas.get(schemaName); JavadocDescription javadoc = Utils.getJavadocParser().parse(type.getDocumentation().orElse(null)); if (schema == null) { @@ -1828,7 +1855,7 @@ private Schema getSchemaDefinition( schema.setEnum(getEnumValues(enumEl, schema.getType(), schema.getFormat(), context)); } } else { - Schema schemaWithSuperTypes = processSuperTypes(null, schemaName, type, definingElement, openAPI, mediaTypes, schemas, context); + Schema schemaWithSuperTypes = processSuperTypes(null, schemaName, type, definingElement, openAPI, mediaTypes, schemas, context, jsonViewClass); if (schemaWithSuperTypes != null) { schema = schemaWithSuperTypes; } @@ -1836,7 +1863,7 @@ private Schema getSchemaDefinition( schema.setDescription(javadoc.getMethodDescription()); } - populateSchemaProperties(openAPI, context, type, typeArgs, schema, mediaTypes, javadoc); + populateSchemaProperties(openAPI, context, type, typeArgs, schema, mediaTypes, javadoc, jsonViewClass); checkAllOf(schema); } } @@ -1844,7 +1871,8 @@ private Schema getSchemaDefinition( return primitiveType.createProperty(); } } else { - String schemaName = schemaValue.stringValue("name").orElse(computeDefaultSchemaName(definingElement, type, typeArgs, context)); + String schemaName = schemaValue.stringValue("name") + .orElse(computeDefaultSchemaName(definingElement, type, typeArgs, context, jsonViewClass)); schema = schemas.get(schemaName); if (schema == null) { if (inProgressSchemas.contains(schemaName)) { @@ -1853,10 +1881,10 @@ private Schema getSchemaDefinition( } inProgressSchemas.add(schemaName); try { - schema = readSchema(schemaValue, openAPI, context, type, typeArgs, mediaTypes); + schema = readSchema(schemaValue, openAPI, context, type, typeArgs, mediaTypes, jsonViewClass); AnnotationValue typeSchema = type.getDeclaredAnnotation(io.swagger.v3.oas.annotations.media.Schema.class); if (typeSchema != null) { - Schema originalTypeSchema = readSchema(typeSchema, openAPI, context, type, typeArgs, mediaTypes); + Schema originalTypeSchema = readSchema(typeSchema, openAPI, context, type, typeArgs, mediaTypes, jsonViewClass); if (originalTypeSchema != null && schema != null) { if (StringUtils.isNotEmpty(originalTypeSchema.getDescription())) { schema.setDescription(originalTypeSchema.getDescription()); @@ -1867,7 +1895,7 @@ private Schema getSchemaDefinition( } if (schema != null) { - processSuperTypes(schema, schemaName, type, definingElement, openAPI, mediaTypes, schemas, context); + processSuperTypes(schema, schemaName, type, definingElement, openAPI, mediaTypes, schemas, context, jsonViewClass); } } catch (JsonProcessingException e) { context.warn("Error reading Swagger Parameter for element [" + type + "]: " + e.getMessage(), type); @@ -1880,7 +1908,7 @@ private Schema getSchemaDefinition( AnnotationValue externalDocsValue = type.getDeclaredAnnotation(io.swagger.v3.oas.annotations.ExternalDocumentation.class); ExternalDocumentation externalDocs = null; if (externalDocsValue != null) { - externalDocs = toValue(externalDocsValue.getValues(), context, ExternalDocumentation.class).orElse(null); + externalDocs = toValue(externalDocsValue.getValues(), context, ExternalDocumentation.class, null).orElse(null); } if (externalDocs != null) { schema.setExternalDocs(externalDocs); @@ -1902,7 +1930,8 @@ private Schema processSuperTypes(Schema schema, OpenAPI openAPI, List mediaTypes, Map schemas, - VisitorContext context) { + VisitorContext context, + @Nullable ClassElement jsonViewClass) { if (type.getName().equals(Object.class.getName())) { return null; @@ -1943,7 +1972,7 @@ private Schema processSuperTypes(Schema schema, if (customStype != null) { sType = customStype; } - readAllInterfaces(openAPI, context, definingElement, mediaTypes, schema, sType, schemas, sTypeArgs); + readAllInterfaces(openAPI, context, definingElement, mediaTypes, schema, sType, schemas, sTypeArgs, jsonViewClass); } } else { if (schema == null) { @@ -1960,12 +1989,13 @@ private Schema processSuperTypes(Schema schema, @SuppressWarnings("java:S3655") // false positive private void readAllInterfaces(OpenAPI openAPI, VisitorContext context, @Nullable Element definingElement, List mediaTypes, - Schema schema, ClassElement superType, Map schemas, Map superTypeArgs) { + Schema schema, ClassElement superType, Map schemas, Map superTypeArgs, + @Nullable ClassElement jsonViewClass) { String parentSchemaName = superType.stringValue(io.swagger.v3.oas.annotations.media.Schema.class, "name") - .orElse(computeDefaultSchemaName(definingElement, superType, superTypeArgs, context)); + .orElse(computeDefaultSchemaName(definingElement, superType, superTypeArgs, context, jsonViewClass)); if (schemas.get(parentSchemaName) != null - || getSchemaDefinition(openAPI, context, superType, superTypeArgs, null, mediaTypes) != null) { + || getSchemaDefinition(openAPI, context, superType, superTypeArgs, null, mediaTypes, jsonViewClass) != null) { Schema parentSchema = new Schema(); parentSchema.set$ref(SchemaUtils.schemaRef(parentSchemaName)); if (schema.getAllOf() == null || !schema.getAllOf().contains(parentSchema)) { @@ -1985,7 +2015,7 @@ private void readAllInterfaces(OpenAPI openAPI, VisitorContext context, @Nullabl interfaceElement = customInterfaceType; } - readAllInterfaces(openAPI, context, definingElement, mediaTypes, schema, interfaceElement, schemas, interfaceTypeArgs); + readAllInterfaces(openAPI, context, definingElement, mediaTypes, schema, interfaceElement, schemas, interfaceTypeArgs, jsonViewClass); } } else if (superType.getSuperType().isPresent()) { ClassElement superSuperType = superType.getSuperType().get(); @@ -1994,20 +2024,20 @@ private void readAllInterfaces(OpenAPI openAPI, VisitorContext context, @Nullabl if (customSuperSuperType != null) { superSuperType = customSuperSuperType; } - readAllInterfaces(openAPI, context, definingElement, mediaTypes, schema, superSuperType, schemas, superSuperTypeArgs); + readAllInterfaces(openAPI, context, definingElement, mediaTypes, schema, superSuperType, schemas, superSuperTypeArgs, jsonViewClass); } } - private void processClassValues(Schema schemaToBind, Map annValues, List mediaTypes, VisitorContext context) { + private void processClassValues(Schema schemaToBind, Map annValues, List mediaTypes, VisitorContext context, @Nullable ClassElement jsonViewClass) { OpenAPI openAPI = Utils.resolveOpenApi(context); final AnnotationClassValue not = (AnnotationClassValue) annValues.get("not"); if (not != null) { - final Schema schemaNot = resolveSchema(null, context.getClassElement(not.getName()).get(), context, Collections.emptyList()); + final Schema schemaNot = resolveSchema(null, context.getClassElement(not.getName()).get(), context, Collections.emptyList(), jsonViewClass); schemaToBind.setNot(schemaNot); } final AnnotationClassValue[] allOf = (AnnotationClassValue[]) annValues.get("allOf"); if (ArrayUtils.isNotEmpty(allOf)) { - List> schemaList = namesToSchemas(openAPI, context, allOf, mediaTypes); + List> schemaList = namesToSchemas(openAPI, context, allOf, mediaTypes, jsonViewClass); for (Schema s : schemaList) { if (TYPE_OBJECT.equals(s.getType())) { if (schemaToBind.getType() == null) { @@ -2022,7 +2052,7 @@ private void processClassValues(Schema schemaToBind, Map a } final AnnotationClassValue[] anyOf = (AnnotationClassValue[]) annValues.get("anyOf"); if (ArrayUtils.isNotEmpty(anyOf)) { - List> schemaList = namesToSchemas(openAPI, context, anyOf, mediaTypes); + List> schemaList = namesToSchemas(openAPI, context, anyOf, mediaTypes, jsonViewClass); for (Schema s : schemaList) { if (TYPE_OBJECT.equals(s.getType())) { if (schemaToBind.getType() == null) { @@ -2037,7 +2067,7 @@ private void processClassValues(Schema schemaToBind, Map a } final AnnotationClassValue[] oneOf = (AnnotationClassValue[]) annValues.get("oneOf"); if (ArrayUtils.isNotEmpty(oneOf)) { - List> schemaList = namesToSchemas(openAPI, context, oneOf, mediaTypes); + List> schemaList = namesToSchemas(openAPI, context, oneOf, mediaTypes, jsonViewClass); for (Schema s : schemaList) { if (TYPE_OBJECT.equals(s.getType())) { if (schemaToBind.getType() == null) { @@ -2062,18 +2092,21 @@ private void processClassValues(Schema schemaToBind, Map a * @param type type element * @param typeArgs type arguments * @param mediaTypes The media types of schema + * @param jsonViewClass Class from JsonView annotation * * @return New schema instance * * @throws JsonProcessingException when Json parsing fails */ @SuppressWarnings("java:S3776") - protected Schema readSchema(AnnotationValue schemaValue, OpenAPI openAPI, VisitorContext context, @Nullable Element type, Map typeArgs, List mediaTypes) throws JsonProcessingException { + protected Schema readSchema(AnnotationValue schemaValue, OpenAPI openAPI, VisitorContext context, + @Nullable Element type, Map typeArgs, List mediaTypes, + @Nullable ClassElement jsonViewClass) throws JsonProcessingException { Map values = schemaValue.getValues() .entrySet() .stream() .collect(toMap(e -> e.getKey().equals("requiredProperties") ? "required" : e.getKey(), Map.Entry::getValue)); - Optional schemaOpt = toValue(values, context, Schema.class); + Optional schemaOpt = toValue(values, context, Schema.class, jsonViewClass); if (!schemaOpt.isPresent()) { return null; } @@ -2113,7 +2146,7 @@ protected Schema readSchema(AnnotationValue getEnumValues(EnumElement type, String schemaType, String s return enumValues; } - private List> namesToSchemas(OpenAPI openAPI, VisitorContext context, AnnotationClassValue[] names, List mediaTypes) { + private List> namesToSchemas(OpenAPI openAPI, VisitorContext context, AnnotationClassValue[] names, List mediaTypes, @Nullable ClassElement jsonViewClass) { return Arrays.stream(names) .flatMap((Function, Stream>>) classAnn -> { final Optional classElementOpt = context.getClassElement(classAnn.getName()); @@ -2172,7 +2205,7 @@ private List> namesToSchemas(OpenAPI openAPI, VisitorContext context, if (customClassElement != null) { classElement = customClassElement; } - final Schema schemaDefinition = getSchemaDefinition(openAPI, context, classElement, classElementTypeArgs, null, mediaTypes); + final Schema schemaDefinition = getSchemaDefinition(openAPI, context, classElement, classElementTypeArgs, null, mediaTypes, jsonViewClass); if (schemaDefinition != null) { return Stream.of(schemaDefinition); } @@ -2182,10 +2215,19 @@ private List> namesToSchemas(OpenAPI openAPI, VisitorContext context, }).collect(Collectors.toList()); } - private String computeDefaultSchemaName(Element definingElement, Element type, Map typeArgs, VisitorContext context) { + private String computeDefaultSchemaName(Element definingElement, Element type, Map typeArgs, VisitorContext context, + @Nullable ClassElement jsonViewClass) { + + String jsonViewPostfix = StringUtils.EMPTY_STRING; + if (jsonViewClass != null) { + String jsonViewClassName = jsonViewClass.getName(); + jsonViewClassName = jsonViewClassName.replaceAll("\\$", "."); + jsonViewPostfix = "_" + (jsonViewClassName.contains(".") ? jsonViewClassName.substring(jsonViewClassName.lastIndexOf('.') + 1) : jsonViewClassName); + } + final String metaAnnName = definingElement == null ? null : definingElement.getAnnotationNameByStereotype(io.swagger.v3.oas.annotations.media.Schema.class).orElse(null); if (metaAnnName != null && !io.swagger.v3.oas.annotations.media.Schema.class.getName().equals(metaAnnName)) { - return NameUtils.getSimpleName(metaAnnName); + return NameUtils.getSimpleName(metaAnnName) + jsonViewPostfix; } String packageName; String resultSchemaName; @@ -2203,7 +2245,7 @@ private String computeDefaultSchemaName(Element definingElement, Element type, M } OpenApiApplicationVisitor.SchemaDecorator schemaDecorator = OpenApiApplicationVisitor.getSchemaDecoration(packageName, context); - resultSchemaName = resultSchemaName.replace("$", "."); + resultSchemaName = resultSchemaName.replaceAll("\\$", ".") + jsonViewPostfix; if (schemaDecorator != null) { resultSchemaName = (StringUtils.hasText(schemaDecorator.getPrefix()) ? schemaDecorator.getPrefix() : StringUtils.EMPTY_STRING) + resultSchemaName @@ -2271,7 +2313,8 @@ static boolean isJavaElement(ClassElement classElement, VisitorContext context) "io.micronaut.annotation.processing.visitor.JavaVisitorContext".equals(context.getClass().getName()); } - private void populateSchemaProperties(OpenAPI openAPI, VisitorContext context, Element type, Map typeArgs, Schema schema, List mediaTypes, JavadocDescription classJavadoc) { + private void populateSchemaProperties(OpenAPI openAPI, VisitorContext context, Element type, Map typeArgs, Schema schema, + List mediaTypes, JavadocDescription classJavadoc, @Nullable ClassElement jsonViewClass) { ClassElement classElement = null; if (type instanceof ClassElement) { classElement = (ClassElement) type; @@ -2288,7 +2331,7 @@ private void populateSchemaProperties(OpenAPI openAPI, VisitorContext context, E // Workaround for https://github.com/micronaut-projects/micronaut-openapi/issues/313 beanProperties = Collections.emptyList(); } - processPropertyElements(openAPI, context, type, typeArgs, schema, beanProperties, mediaTypes, classJavadoc); + processPropertyElements(openAPI, context, type, typeArgs, schema, beanProperties, mediaTypes, classJavadoc, jsonViewClass); String visibilityLevelProp = getConfigurationProperty(MICRONAUT_OPENAPI_FIELD_VISIBILITY_LEVEL, context); VisibilityLevel visibilityLevel = VisibilityLevel.PUBLIC; @@ -2329,12 +2372,14 @@ private void populateSchemaProperties(OpenAPI openAPI, VisitorContext context, E publicFields.add(field); } - processPropertyElements(openAPI, context, type, typeArgs, schema, publicFields, mediaTypes, classJavadoc); + processPropertyElements(openAPI, context, type, typeArgs, schema, publicFields, mediaTypes, classJavadoc, jsonViewClass); } } @SuppressWarnings("java:S3776") - private void processPropertyElements(OpenAPI openAPI, VisitorContext context, Element type, Map typeArgs, Schema schema, List publicFields, List mediaTypes, JavadocDescription classJavadoc) { + private void processPropertyElements(OpenAPI openAPI, VisitorContext context, Element type, Map typeArgs, Schema schema, + List publicFields, List mediaTypes, JavadocDescription classJavadoc, + @Nullable ClassElement jsonViewClass) { ClassElement classElement = null; if (type instanceof ClassElement) { @@ -2343,6 +2388,11 @@ private void processPropertyElements(OpenAPI openAPI, VisitorContext context, El classElement = ((TypedElement) type).getType(); } + boolean withJsonView = jsonViewClass != null; + List classLvlJsonViewClasses = null; + if (withJsonView) { + classLvlJsonViewClasses = ElementUtils.getJsonViewClasses(classElement); + } for (TypedElement publicField : publicFields) { boolean isHidden = publicField.getAnnotationMetadata().booleanValue(io.swagger.v3.oas.annotations.media.Schema.class, "hidden").orElse(false); @@ -2354,6 +2404,10 @@ private void processPropertyElements(OpenAPI openAPI, VisitorContext context, El continue; } + if (withJsonView && notAllowedByJsonView(publicField, classLvlJsonViewClasses, jsonViewClass)) { + continue; + } + JavadocDescription fieldJavadoc = null; if (classElement != null) { for (FieldElement field : classElement.getFields()) { @@ -2374,7 +2428,11 @@ private void processPropertyElements(OpenAPI openAPI, VisitorContext context, El } } - Schema propertySchema = resolveSchema(openAPI, publicField, fieldType, context, mediaTypes, fieldJavadoc, classJavadoc); + if (withJsonView && notAllowedByJsonView(publicField, classLvlJsonViewClasses, jsonViewClass)) { + continue; + } + + Schema propertySchema = resolveSchema(openAPI, publicField, fieldType, context, mediaTypes, jsonViewClass, fieldJavadoc, classJavadoc); processSchemaProperty( context, @@ -2388,6 +2446,41 @@ private void processPropertyElements(OpenAPI openAPI, VisitorContext context, El } } + private boolean notAllowedByJsonView(TypedElement publicField, List classLvlJsonViewClasses, ClassElement jsonViewClassEl) { + List fieldJsonViewClasses = ElementUtils.getJsonViewClasses(publicField); + if (CollectionUtils.isEmpty(fieldJsonViewClasses)) { + fieldJsonViewClasses = classLvlJsonViewClasses; + } + return CollectionUtils.isNotEmpty(fieldJsonViewClasses) && !checkJsonView(fieldJsonViewClasses, jsonViewClassEl); + } + + private boolean checkJsonView(List fieldJsonViewClasses, ClassElement jsonViewClassEl) { + + if (fieldJsonViewClasses.contains(jsonViewClassEl.getName())) { + return true; + } + + if (jsonViewClassEl.isInterface()) { + Collection superInterfaces = jsonViewClassEl.getInterfaces(); + if (CollectionUtils.isEmpty(superInterfaces)) { + return false; + } + for (ClassElement superInterface : superInterfaces) { + boolean found = checkJsonView(fieldJsonViewClasses, superInterface); + if (found) { + return true; + } + } + } else { + ClassElement superType = jsonViewClassEl.getSuperType().orElse(null); + if (superType != null) { + return checkJsonView(fieldJsonViewClasses, superType); + } + } + + return false; + } + private Schema getPrimitiveType(String typeName) { Schema schema = null; Optional aClass = ClassUtils.getPrimitiveType(typeName); @@ -2420,7 +2513,7 @@ protected void processSecuritySchemes(ClassElement element, VisitorContext conte final OpenAPI openAPI = Utils.resolveOpenApi(context); for (AnnotationValue securityRequirementAnnotationValue : values) { - final Map map = toValueMap(securityRequirementAnnotationValue.getValues(), context); + final Map map = toValueMap(securityRequirementAnnotationValue.getValues(), context, null); securityRequirementAnnotationValue.stringValue("name") .ifPresent(name -> { @@ -2464,7 +2557,7 @@ protected void processSecuritySchemes(ClassElement element, VisitorContext conte } try { - JsonNode node = toJson(map, context); + JsonNode node = toJson(map, context, null); SecurityScheme securityScheme = ConvertUtils.treeToValue(node, SecurityScheme.class, context); if (securityScheme != null) { resolveExtensions(node).ifPresent(extensions -> BeanMap.of(securityScheme).put("extensions", extensions)); @@ -2516,7 +2609,7 @@ protected List processOpenApiAnnotation(Element ele } else { values = tag.getValues(); } - Optional tagOpt = toValue(values, context, modelType); + Optional tagOpt = toValue(values, context, modelType, null); if (tagOpt.isPresent()) { T tagObj = tagOpt.get(); // skip all existed tags diff --git a/openapi/src/main/java/io/micronaut/openapi/visitor/ElementUtils.java b/openapi/src/main/java/io/micronaut/openapi/visitor/ElementUtils.java index f7d20d1440..5952015d78 100644 --- a/openapi/src/main/java/io/micronaut/openapi/visitor/ElementUtils.java +++ b/openapi/src/main/java/io/micronaut/openapi/visitor/ElementUtils.java @@ -18,16 +18,25 @@ import java.io.File; import java.io.InputStream; import java.nio.ByteBuffer; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.concurrent.Future; +import io.micronaut.core.annotation.AnnotationClassValue; +import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.util.ArrayUtils; +import io.micronaut.core.util.CollectionUtils; import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.Element; import io.micronaut.inject.ast.TypedElement; +import com.fasterxml.jackson.annotation.JsonView; + /** * Some util methods. * @@ -158,4 +167,33 @@ private static boolean findAnyAssignable(ClassElement type, List typeNam } return false; } + + /** + * Read classnames from JsonView annotation. + * + * @param element The element + * + * @return List of classnames from JsonView annotation. + */ + @Nullable + public static List getJsonViewClasses(@Nullable Element element) { + AnnotationValue classLvlJsonView = element != null ? element.getAnnotation(JsonView.class) : null; + if (classLvlJsonView == null) { + return null; + } + Map values = classLvlJsonView.getValues(); + if (CollectionUtils.isEmpty(values)) { + return null; + } + AnnotationClassValue[] test = (AnnotationClassValue[]) values.get("value"); + if (ArrayUtils.isEmpty(test)) { + return null; + } + List jsonViewClasses = new ArrayList<>(test.length); + for (AnnotationClassValue value : test) { + jsonViewClasses.add(value.getName()); + } + + return jsonViewClasses; + } } diff --git a/openapi/src/main/java/io/micronaut/openapi/visitor/OpenApiApplicationVisitor.java b/openapi/src/main/java/io/micronaut/openapi/visitor/OpenApiApplicationVisitor.java index 24cb63fbf9..53f9929132 100644 --- a/openapi/src/main/java/io/micronaut/openapi/visitor/OpenApiApplicationVisitor.java +++ b/openapi/src/main/java/io/micronaut/openapi/visitor/OpenApiApplicationVisitor.java @@ -260,10 +260,18 @@ public class OpenApiApplicationVisitor extends AbstractOpenApiVisitor implements * Final calculated openapi filenames. */ public static final String MICRONAUT_INTERNAL_OPENAPI_FILENAMES = "micronaut.internal.openapi.filenames"; + /** + * Loaded micronaut-http-server-netty property (json-view.enabled). + */ + public static final String MICRONAUT_JACKSON_JSON_VIEW_ENABLED = "jackson.json-view.enabled"; /** * Loaded micronaut environment. */ private static final String MICRONAUT_ENVIRONMENT = "micronaut.environment"; + /** + * Loaded into context jackson.json-view.enabled property value. + */ + private static final String MICRONAUT_INTERNAL_JACKSON_JSON_VIEW_ENABLED = "micronaut.internal.jackson.json-view.enabled"; private static final String MICRONAUT_ENVIRONMENT_CREATED = "micronaut.environment.created"; private static final String MICRONAUT_OPENAPI_PROPERTIES = "micronaut.openapi.properties"; private static final String MICRONAUT_OPENAPI_ENDPOINTS = "micronaut.openapi.endpoints"; @@ -618,6 +626,19 @@ public static String getConfigurationProperty(String key, VisitorContext context return environment != null ? environment.get(key, String.class).orElse(null) : null; } + public static boolean isJsonViewEnabled(VisitorContext context) { + + Boolean isJsonViewEnabled = context.get(MICRONAUT_INTERNAL_JACKSON_JSON_VIEW_ENABLED, Boolean.class).orElse(null); + if (isJsonViewEnabled != null) { + return isJsonViewEnabled; + } + + isJsonViewEnabled = getBooleanProperty(MICRONAUT_JACKSON_JSON_VIEW_ENABLED, false, context); + context.put(MICRONAUT_INTERNAL_JACKSON_JSON_VIEW_ENABLED, isJsonViewEnabled); + + return isJsonViewEnabled; + } + public static SecurityProperties getSecurityProperties(VisitorContext context) { SecurityProperties securityProperties = context.get(MICRONAUT_INTERNAL_SECURITY_PROPERTIES, SecurityProperties.class).orElse(null); @@ -1070,7 +1091,7 @@ private void copyOpenApi(OpenAPI to, OpenAPI from) { private OpenAPI readOpenApi(ClassElement element, VisitorContext context) { return element.findAnnotation(OpenAPIDefinition.class).flatMap(o -> { - Optional result = toValue(o.getValues(), context, OpenAPI.class); + Optional result = toValue(o.getValues(), context, OpenAPI.class, null); result.ifPresent(openAPI -> { List securityRequirements = o.getAnnotations("security", SecurityRequirement.class) diff --git a/openapi/src/main/java/io/micronaut/openapi/visitor/OpenApiGroupInfoVisitor.java b/openapi/src/main/java/io/micronaut/openapi/visitor/OpenApiGroupInfoVisitor.java index edee060c67..65bccb4a40 100644 --- a/openapi/src/main/java/io/micronaut/openapi/visitor/OpenApiGroupInfoVisitor.java +++ b/openapi/src/main/java/io/micronaut/openapi/visitor/OpenApiGroupInfoVisitor.java @@ -31,7 +31,6 @@ import io.micronaut.inject.visitor.TypeElementVisitor; import io.micronaut.inject.visitor.VisitorContext; import io.micronaut.openapi.annotation.OpenAPIGroupInfo; -import io.micronaut.openapi.annotation.OpenAPIInclude; import io.swagger.v3.oas.annotations.OpenAPIDefinition; import io.swagger.v3.oas.models.OpenAPI; diff --git a/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiJsonViewSpec.groovy b/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiJsonViewSpec.groovy new file mode 100644 index 0000000000..ba45b7fe7b --- /dev/null +++ b/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiJsonViewSpec.groovy @@ -0,0 +1,230 @@ +package io.micronaut.openapi.visitor + +import io.micronaut.openapi.AbstractOpenApiTypeElementSpec +import io.swagger.v3.oas.models.OpenAPI +import io.swagger.v3.oas.models.Operation +import io.swagger.v3.oas.models.media.Schema + +class OpenApiJsonViewSpec extends AbstractOpenApiTypeElementSpec { + + void "test build OpenAPI with JsonView"() { + + setup: + System.setProperty(OpenApiApplicationVisitor.MICRONAUT_JACKSON_JSON_VIEW_ENABLED, "true") + + when: + buildBeanDefinition('test.MyBean', ''' +package test; + +import java.util.List; + +import io.micronaut.http.HttpResponse; +import io.micronaut.http.annotation.Body; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Post; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; + +import com.fasterxml.jackson.annotation.JsonView; + +@Controller +class OpenApiController { + + @Get("/summary") + @JsonView(View.Summary.class) + @Operation(summary = "Return car summaries", + responses = @ApiResponse(responseCode = "200", content = @Content(array = @ArraySchema(schema = @Schema(implementation = Car.class))))) + public HttpResponse getSummaries() { + return null; + } + + @Get("/detail") + @JsonView(View.Detail.class) + @ApiResponse(responseCode = "200", description = "Return car detail", content = @Content(schema = @Schema(implementation = Car.class))) + @Operation(summary = "Return car detail") + public List getDetails() { + return null; + } + + /** + * {@summary Return car sale summary} + */ + @Get("/sale") + @JsonView(View.Sale.class) + public List getSaleSummaries() { + return null; + } + + @Post("/add") + public void addCar(@JsonView(View.Sale.class) @Body Car car) { + } +} + +interface View { + + interface Summary {} + + interface Detail extends Summary {} + + interface Sale {} +} + +class Car { + + @JsonView(View.Summary.class) + private String made; + + @JsonView({View.Summary.class, View.Detail.class}) + private String model; + + @JsonView(View.Detail.class) + private List tires; + + @JsonView(View.Sale.class) + private int price; + + @JsonView({View.Sale.class, View.Summary.class}) + private int age; + + // common + private String color; + + public String getColor() { + return color; + } + + public String getMade() { + return made; + } + + public void setMade(String made) { + this.made = made; + } + + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } + + public List getTires() { + return tires; + } + + public void setTires(List tires) { + this.tires = tires; + } + + public int getPrice() { + return price; + } + + public void setPrice(int price) { + this.price = price; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public void setColor(String color) { + this.color = color; + } +} + +class Tire { + + @JsonView(View.Summary.class) + private String made; + + @JsonView(View.Detail.class) + private String condition; + + public String getMade() { + return made; + } + + public void setMade(String made) { + this.made = made; + } + + public String getCondition() { + return condition; + } + + public void setCondition(String condition) { + this.condition = condition; + } +} + +@jakarta.inject.Singleton +class MyBean {} +''') + then: "the state is correct" + Utils.testReference != null + + when: "The OpenAPI is retrieved" + OpenAPI openAPI = Utils.testReference + Schema carDetail = openAPI.components.schemas['Car_Detail'] + Schema carSale = openAPI.components.schemas['Car_Sale'] + Schema carSummary = openAPI.components.schemas['Car_Summary'] + Schema tireDetail = openAPI.components.schemas['Tire_Detail'] + Operation addOp = openAPI.paths."/add".post + Operation detailOp = openAPI.paths."/detail".get + Operation saleOp = openAPI.paths."/sale".get + Operation summaryOp = openAPI.paths."/summary".get + + then: + + addOp + addOp.requestBody.content.'application/json'.schema.$ref == '#/components/schemas/Car_Sale' + + detailOp + detailOp.responses.'200'.content.'application/json'.schema.$ref == '#/components/schemas/Car_Detail' + + saleOp + saleOp.responses.'200'.content.'application/json'.schema.items.$ref == '#/components/schemas/Car_Sale' + + summaryOp + summaryOp.responses.'200'.content.'application/json'.schema.items.$ref == '#/components/schemas/Car_Summary' + + carDetail + carDetail.properties.size() == 5 + carDetail.properties.color + carDetail.properties.made + carDetail.properties.model + carDetail.properties.tires + carDetail.properties.age + + carSale + carSale.properties.size() == 3 + carSale.properties.color + carSale.properties.price + carSale.properties.age + + carSummary + carSummary.properties.size() == 4 + carSummary.properties.color + carSummary.properties.made + carSummary.properties.model + carSummary.properties.age + + tireDetail + tireDetail.properties.size() == 2 + tireDetail.properties.made + tireDetail.properties.condition + + cleanup: + System.clearProperty(OpenApiApplicationVisitor.MICRONAUT_JACKSON_JSON_VIEW_ENABLED) + } +}