diff --git a/core/src/main/java/io/smallrye/openapi/runtime/io/schema/SchemaFactory.java b/core/src/main/java/io/smallrye/openapi/runtime/io/schema/SchemaFactory.java index 743d6aa43..c63ae315d 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/io/schema/SchemaFactory.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/io/schema/SchemaFactory.java @@ -93,8 +93,9 @@ public static Schema readSchema(final AnnotationScannerContext context, Annotati public static Schema readSchema(final AnnotationScannerContext context, Schema schema, AnnotationInstance annotation, - ClassInfo clazz) { - return readSchema(context, schema, annotation, clazz, Collections.emptyMap()); + ClassInfo clazz, + boolean registerSchema) { + return readSchema(context, schema, annotation, clazz, registerSchema, Collections.emptyMap()); } /** @@ -113,6 +114,7 @@ static Schema readSchema(final AnnotationScannerContext context, Schema schema, AnnotationInstance annotation, ClassInfo clazz, + boolean registerSchema, Map defaults) { if (isAnnotationMissingOrHidden(context, annotation, defaults)) { @@ -129,7 +131,9 @@ static Schema readSchema(final AnnotationScannerContext context, * * Ignore the reference returned by register, the caller expects the full schema. */ - schemaRegistration(context, clazzType, schema); + if (registerSchema) { + schemaRegistration(context, clazzType, schema); + } return schema; } @@ -641,7 +645,7 @@ public static Schema enumToSchema(final AnnotationScannerContext context, Type e Map defaults = new LinkedHashMap<>(TypeUtil.getTypeAttributes(enumValueType)); defaults.put(SchemaConstant.PROP_ENUMERATION, enumeration); - enumSchema = readSchema(context, enumSchema, schemaAnnotation, enumKlazz, defaults); + enumSchema = readSchema(context, enumSchema, schemaAnnotation, enumKlazz, true, defaults); } else { TypeUtil.applyTypeAttributes(enumValueType, enumSchema); enumSchema.setEnumeration(enumeration); diff --git a/core/src/main/java/io/smallrye/openapi/runtime/scanner/OpenApiDataObjectScanner.java b/core/src/main/java/io/smallrye/openapi/runtime/scanner/OpenApiDataObjectScanner.java index 2bc1d6f5b..d6b36ba5e 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/scanner/OpenApiDataObjectScanner.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/scanner/OpenApiDataObjectScanner.java @@ -34,6 +34,7 @@ import io.smallrye.openapi.runtime.scanner.dataobject.AnnotationTargetProcessor; import io.smallrye.openapi.runtime.scanner.dataobject.AugmentedIndexView; import io.smallrye.openapi.runtime.scanner.dataobject.DataObjectDeque; +import io.smallrye.openapi.runtime.scanner.dataobject.IgnoreResolver; import io.smallrye.openapi.runtime.scanner.dataobject.TypeResolver; import io.smallrye.openapi.runtime.scanner.spi.AnnotationScannerContext; import io.smallrye.openapi.runtime.util.TypeUtil; @@ -246,16 +247,19 @@ private void depthFirstGraphSearch() { ClassInfo currentClass = currentPathEntry.getClazz(); Schema currentSchema = currentPathEntry.getSchema(); + AnnotationTarget currentTarget = currentPathEntry.getAnnotationTarget(); + boolean allowRegistration = !IgnoreResolver.configuresVisibility(context, currentTarget); // First, handle class annotations (re-assign since readKlass may return new schema) - currentSchema = readKlass(currentClass, currentType, currentSchema); + currentSchema = readKlass(currentClass, currentType, currentSchema, allowRegistration); + TypeUtil.mapDeprecated(context, currentClass, currentSchema::getDeprecated, currentSchema::setDeprecated); currentPathEntry.setSchema(currentSchema); if (!hasNonNullType(currentSchema)) { // If not schema has yet been set, consider this an "object" SchemaSupport.setType(currentSchema, Schema.SchemaType.OBJECT); - } else { + } else if (allowRegistration) { maybeRegisterSchema(currentType, currentSchema, entrySchema); } @@ -364,14 +368,15 @@ private void encloseCurrentSchema(Schema currentSchema, Type currentType, DataOb private Schema readKlass(ClassInfo currentClass, Type currentType, - Schema currentSchema) { + Schema currentSchema, + boolean registerSchema) { AnnotationInstance annotation = TypeUtil.getSchemaAnnotation(context, currentClass); Schema classSchema; if (annotation != null) { // Because of implementation= field, *may* return a new schema rather than modify. - classSchema = SchemaFactory.readSchema(context, currentSchema, annotation, currentClass); + classSchema = SchemaFactory.readSchema(context, currentSchema, annotation, currentClass, registerSchema); } else if (isA(currentType, ENUM_TYPE)) { classSchema = SchemaFactory.enumToSchema(context, currentType); } else { diff --git a/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/IgnoreResolver.java b/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/IgnoreResolver.java index bb799f878..0b37f0c86 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/IgnoreResolver.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/IgnoreResolver.java @@ -78,6 +78,40 @@ public Visibility referenceVisibility(String propertyName, return Visibility.UNSET; } + /** + * Check whether the given target contains annotations that configure the + * visibility of the properties of the type annotated. + * + * This is used to only allow registration of a type if the annotation + * target (field or method) that refers to the type's class is not annotated + * with an annotation that alters the visibility of fields in the class. + * + *

+ * For example, in this scenario when we process `fieldB`, registration of + * class B's schema will not occur because it's definition is altered by and + * specific to its use in class A. + * + *

+     * 
+     * class A {
+     *   @JsonIgnoreProperties({"field2"})
+     *   B fieldB;
+     * }
+     *
+     * class B {
+     *   int field1;
+     *   int field2;
+     * }
+     * 
+     * 
+ */ + public static boolean configuresVisibility(AnnotationScannerContext context, AnnotationTarget target) { + if (target != null) { + return context.getIgnoreResolver().configuresVisibility(target); + } + return false; + } + public boolean configuresVisibility(AnnotationTarget reference) { for (IgnoreAnnotationHandler handler : ignoreHandlers) { if (handler.configuresVisibility(reference)) { diff --git a/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/TypeProcessor.java b/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/TypeProcessor.java index bd75f5021..3f4c0fcc7 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/TypeProcessor.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/TypeProcessor.java @@ -155,27 +155,10 @@ public Type processType() { * refers to the type's class is not annotated with an annotation that alters * the visibility of fields in the class. * - *

- * For example, in this scenario when we process `fieldB`, registration of class B's - * schema will not occur because it's definition is altered by and specific to its - * use in class A. - * - *

-     * 
-     * class A {
-     *   @JsonIgnoreProperties({"field2"})
-     *   B fieldB;
-     * }
-     *
-     * class B {
-     *   int field1;
-     *   int field2;
-     * }
-     * 
-     * 
+ * @see IgnoreResolver#configuresVisibility(AnnotationScannerContext, AnnotationTarget) */ public boolean allowRegistration() { - return !context.getIgnoreResolver().configuresVisibility(annotationTarget); + return !IgnoreResolver.configuresVisibility(context, annotationTarget); } private Type readArrayType(ArrayType arrayType, Schema arraySchema) { diff --git a/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/TypeResolver.java b/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/TypeResolver.java index 8388d4611..2f2e939a4 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/TypeResolver.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/TypeResolver.java @@ -41,7 +41,6 @@ import io.smallrye.openapi.api.constants.JaxbConstants; import io.smallrye.openapi.api.constants.JsonbConstants; import io.smallrye.openapi.runtime.io.schema.SchemaConstant; -import io.smallrye.openapi.runtime.scanner.dataobject.IgnoreResolver.Visibility; import io.smallrye.openapi.runtime.scanner.spi.AnnotationScannerContext; import io.smallrye.openapi.runtime.util.Annotations; import io.smallrye.openapi.runtime.util.JandexUtil; @@ -581,47 +580,6 @@ private static boolean isNonPublicOrAbsent(MethodInfo method) { return method == null || !Modifier.isPublic(method.flags()); } - private static boolean isViewable(AnnotationScannerContext context, AnnotationTarget propertySource) { - Map activeViews = context.getJsonViews(); - - if (activeViews.isEmpty()) { - return true; - } - - Type[] applicableViews = getJsonViews(context, propertySource); - - if (applicableViews != null && applicableViews.length > 0) { - return Arrays.stream(applicableViews).anyMatch(activeViews::containsKey); - } - - return true; - } - - /** - * Find {@literal @}JsonView annotations on the field/method, or on the - * declaring class when the field/method itself it not directly targeted by the - * annotation. - */ - private static Type[] getJsonViews(AnnotationScannerContext context, AnnotationTarget propertySource) { - Annotations annotations = context.annotations(); - - Type[] directViews = annotations.getAnnotationValue(propertySource, JacksonConstants.JSON_VIEW); - - if (directViews != null) { - return directViews; - } - - ClassInfo declaringClass; - - if (propertySource.kind() == Kind.FIELD) { - declaringClass = propertySource.asField().declaringClass(); - } else { - declaringClass = propertySource.asMethod().declaringClass(); - } - - return annotations.getAnnotationValue(declaringClass, JacksonConstants.JSON_VIEW); - } - /** * Determine if the target should be exposed in the API or ignored. Explicitly un-hiding a property * via the @Schema annotation overrides configuration to ignore the same property @@ -638,12 +596,6 @@ private void processVisibility(AnnotationScannerContext context, AnnotationTarge return; } - if (this.isUnhidden(target, reference)) { - // @Schema with hidden = false and not already ignored by a member lower in the class hierarchy - this.exposed = true; - return; - } - switch (getVisibility(context, target, reference, descendants, propertyName)) { case EXPOSED: this.exposed = true; @@ -706,13 +658,26 @@ private IgnoreResolver.Visibility getVisibility(AnnotationScannerContext context List descendants, String propertyName) { + IgnoreResolver.Visibility visibility; + if (!isViewable(context, target)) { return IgnoreResolver.Visibility.IGNORED; } IgnoreResolver ignoreResolver = context.getIgnoreResolver(); + visibility = ignoreResolver.referenceVisibility(propertyName, target, reference); + + if (visibility != IgnoreResolver.Visibility.UNSET) { + return visibility; + } + + if (isUnhidden(target)) { + // @Schema with hidden = false and not already ignored by a member lower in the class hierarchy + return IgnoreResolver.Visibility.EXPOSED; + } + // First check if a descendant class has hidden/exposed the property - IgnoreResolver.Visibility visibility = ignoreResolver.getDescendantVisibility(propertyName, descendants); + visibility = ignoreResolver.getDescendantVisibility(propertyName, descendants); if (visibility == IgnoreResolver.Visibility.UNSET) { visibility = ignoreResolver.isIgnore(propertyName, target, reference); @@ -721,6 +686,47 @@ private IgnoreResolver.Visibility getVisibility(AnnotationScannerContext context return visibility; } + private static boolean isViewable(AnnotationScannerContext context, AnnotationTarget propertySource) { + Map activeViews = context.getJsonViews(); + + if (activeViews.isEmpty()) { + return true; + } + + Type[] applicableViews = getJsonViews(context, propertySource); + + if (applicableViews != null && applicableViews.length > 0) { + return Arrays.stream(applicableViews).anyMatch(activeViews::containsKey); + } + + return true; + } + + /** + * Find {@literal @}JsonView annotations on the field/method, or on the + * declaring class when the field/method itself it not directly targeted by the + * annotation. + */ + private static Type[] getJsonViews(AnnotationScannerContext context, AnnotationTarget propertySource) { + Annotations annotations = context.annotations(); + + AnnotationInstance jsonView = annotations.getAnnotation(propertySource, JacksonConstants.JSON_VIEW); + + if (jsonView != null) { + return annotations.value(jsonView); + } + + ClassInfo declaringClass; + + if (propertySource.kind() == Kind.FIELD) { + declaringClass = propertySource.asField().declaringClass(); + } else { + declaringClass = propertySource.asMethod().declaringClass(); + } + + return annotations.getAnnotationValue(declaringClass, JacksonConstants.JSON_VIEW); + } + /** * Determines whether the target is set to hidden = false via the @Schema * annotation (explicit or default value). A field is only considered un-hidden if its @@ -728,16 +734,16 @@ private IgnoreResolver.Visibility getVisibility(AnnotationScannerContext context * * @return true if the field is un-hidden, false otherwise */ - boolean isUnhidden(AnnotationTarget target, AnnotationTarget reference) { + boolean isUnhidden(AnnotationTarget target) { if (target != null) { - AnnotationInstance schemaAnnotation = TypeUtil.getSchemaAnnotation(context, target); + Annotations annotations = context.annotations(); + AnnotationInstance schema = annotations.getAnnotation(target, SchemaConstant.DOTNAME_SCHEMA); - if (schemaAnnotation != null) { - Boolean hidden = context.annotations().value(schemaAnnotation, SchemaConstant.PROP_HIDDEN); + if (schema != null) { + Boolean hidden = annotations.value(schema, SchemaConstant.PROP_HIDDEN); if (hidden == null || !hidden.booleanValue()) { - return context.getIgnoreResolver().referenceVisibility(propertyName, target, - reference) != Visibility.IGNORED; + return true; } } } diff --git a/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/JsonViewTests.java b/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/JsonViewTests.java index 7a7c01e01..0b4a7131e 100644 --- a/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/JsonViewTests.java +++ b/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/JsonViewTests.java @@ -136,10 +136,6 @@ class Role { private UUID id; @JsonView(Views.Ingest.class) private String name; - @JsonView(Views.Full.class) - private LocalDateTime createdAt; - // Adding @Schema() here does fix the problem were @JsonIgnoreProperties in Group was removing description gobally. - // but now the @JsonView is being ignored and description field is in all JsonViews. @Schema(title = "Title of description") @JsonView(Views.Full.class) private String description; @@ -152,13 +148,10 @@ class Group { @JsonView(Views.Abridged.class) private String name; @JsonView(Views.Full.class) - private LocalDateTime createdAt; - @JsonView(Views.Full.class) private String description; @JsonView(Views.Ingest.class) private String roleId; @JsonView(Views.Abridged.class) - // @JsonIgnoreProperties should only apply to this the Group entity use case of Role. Currently, it's global. @JsonIgnoreProperties("description") private List roles; } @@ -199,8 +192,41 @@ public Response post(@RequestBody @JsonView(Views.Ingest.class) User group) { } } + @Path("/role") + class RoleResource { + @GET + @Produces(MediaType.APPLICATION_JSON) + @JsonView(Views.Full.class) + @APIResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = Role.class))) + public Response getRole() { + return null; + } + + @POST + public Response post(@RequestBody @JsonView(Views.Ingest.class) Role role) { + return null; + } + } + + @Path("/group") + class GroupResource { + @GET + @Produces(MediaType.APPLICATION_JSON) + @JsonView(Views.Full.class) + @APIResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = Group.class))) + public Response get() { + return null; + } + + @POST + public Response post(@RequestBody @JsonView(Views.Ingest.class) Group group) { + return null; + } + } + Index index = Index.of(Views.class, Views.Max.class, Views.Full.class, Views.Ingest.class, Views.Abridged.class, - User.class, Group.class, Role.class, UserResource.class, LocalDateTime.class); + User.class, Group.class, Role.class, UserResource.class, LocalDateTime.class, RoleResource.class, + GroupResource.class); OpenApiConfig config = dynamicConfig(SmallRyeOASConfig.SMALLRYE_REMOVE_UNUSED_SCHEMAS, Boolean.TRUE); OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(config, index); diff --git a/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/special.jsonview-with-ignored.json b/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/special.jsonview-with-ignored.json index 2814be78c..c456ccc29 100644 --- a/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/special.jsonview-with-ignored.json +++ b/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/special.jsonview-with-ignored.json @@ -13,9 +13,6 @@ "name" : { "type" : "string" }, - "createdAt" : { - "$ref" : "#/components/schemas/LocalDateTime" - }, "description" : { "type" : "string" }, @@ -34,9 +31,6 @@ }, "name" : { "type" : "string" - }, - "createdAt" : { - "$ref" : "#/components/schemas/LocalDateTime" } } } @@ -70,6 +64,31 @@ "format" : "date-time", "examples" : [ "2022-03-10T12:15:50" ] }, + "Role_Full" : { + "type" : "object", + "properties" : { + "id" : { + "type" : "string", + "format" : "uuid", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + }, + "name" : { + "type" : "string" + }, + "description" : { + "type" : "string", + "title" : "Title of description" + } + } + }, + "Role_Ingest" : { + "type" : "object", + "properties" : { + "name" : { + "type" : "string" + } + } + }, "User_Full" : { "type" : "object", "properties" : { @@ -107,15 +126,78 @@ "description" : "test date-time field", "type" : "string", "$ref" : "#/components/schemas/LocalDateTime" - }, - "group" : { - "$ref" : "#/components/schemas/Group_Ingest" } } } } }, "paths" : { + "/group" : { + "post" : { + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/Group_Ingest" + } + } + }, + "required" : true + }, + "responses" : { + "200" : { + "description" : "OK" + } + } + }, + "get" : { + "responses" : { + "200" : { + "description" : "OK", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Group_Full" + } + } + } + } + } + } + }, + "/role" : { + "post" : { + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/Role_Ingest" + } + } + }, + "required" : true + }, + "responses" : { + "200" : { + "description" : "OK" + } + } + }, + "get" : { + "responses" : { + "200" : { + "description" : "OK", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Role_Full" + } + } + } + } + } + } + }, "/user" : { "post" : { "requestBody" : {