diff --git a/README.adoc b/README.adoc index a59b6aa77..e2092cc62 100644 --- a/README.adoc +++ b/README.adoc @@ -87,3 +87,11 @@ Set to `FAIL` to abort in case of duplicate operationIds, set to `WARN` to log w mp.openapi.extensions.smallrye.maximumStaticFileSize ---- Set this value in order to change the maximum threshold for processed static files, when generating model from them. If not set, it will default to 3 MB. + +* Merge Schema Examples ++ +[source%nowrap] +---- +mp.openapi.extensions.smallrye.merge-schema-examples +---- +Set this boolean value to disable the merging of the deprecated `@Schema` `example` property into the `examples` array introduced in OAS 3.1.0. If not set, it will default to `true` the deprecated `example` will be mapped to the `examples` array in the OpenAPI model. diff --git a/core/src/main/java/io/smallrye/openapi/api/OpenApiConfig.java b/core/src/main/java/io/smallrye/openapi/api/OpenApiConfig.java index 78edc13f2..b5f3c1c40 100644 --- a/core/src/main/java/io/smallrye/openapi/api/OpenApiConfig.java +++ b/core/src/main/java/io/smallrye/openapi/api/OpenApiConfig.java @@ -302,6 +302,10 @@ default boolean removeUnusedSchemas() { return getConfigValue(SmallRyeOASConfig.SMALLRYE_REMOVE_UNUSED_SCHEMAS, Boolean.class, () -> Boolean.FALSE); } + default boolean mergeSchemaExamples() { + return getConfigValue(SmallRyeOASConfig.SMALLRYE_MERGE_SCHEMA_EXAMPLES, Boolean.class, () -> Boolean.TRUE); + } + default Integer getMaximumStaticFileSize() { return getConfigValue(SmallRyeOASConfig.MAXIMUM_STATIC_FILE_SIZE, Integer.class, () -> MAXIMUM_STATIC_FILE_SIZE_DEFAULT); diff --git a/core/src/main/java/io/smallrye/openapi/api/SmallRyeOASConfig.java b/core/src/main/java/io/smallrye/openapi/api/SmallRyeOASConfig.java index 93fb00160..931c7981b 100644 --- a/core/src/main/java/io/smallrye/openapi/api/SmallRyeOASConfig.java +++ b/core/src/main/java/io/smallrye/openapi/api/SmallRyeOASConfig.java @@ -23,6 +23,7 @@ private SmallRyeOASConfig() { private static final String SUFFIX_PROPERTY_NAMING_STRATEGY = "property-naming-strategy"; private static final String SUFFIX_SORTED_PROPERTIES_ENABLE = "sorted-properties.enable"; private static final String SUFFIX_REMOVE_UNUSED_SCHEMAS_ENABLE = "remove-unused-schemas.enable"; + private static final String SUFFIX_MERGE_SCHEMA_EXAMPLES = "merge-schema-examples"; private static final String SMALLRYE_PREFIX = OASConfig.EXTENSIONS_PREFIX + VENDOR_NAME; public static final String SMALLRYE_SCAN_DEPENDENCIES_DISABLE = SMALLRYE_PREFIX + SUFFIX_SCAN_DEPENDENCIES_DISABLE; @@ -49,6 +50,8 @@ private SmallRyeOASConfig() { public static final String SMALLRYE_REMOVE_UNUSED_SCHEMAS = SMALLRYE_PREFIX + SUFFIX_REMOVE_UNUSED_SCHEMAS_ENABLE; + public static final String SMALLRYE_MERGE_SCHEMA_EXAMPLES = SMALLRYE_PREFIX + SUFFIX_MERGE_SCHEMA_EXAMPLES; + public static final String SCAN_PROFILES = SMALLRYE_PREFIX + "scan.profiles"; public static final String SCAN_EXCLUDE_PROFILES = SMALLRYE_PREFIX + "scan.exclude.profiles"; 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 469b6f8ac..12f7ebf5a 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 @@ -208,19 +208,30 @@ public static Schema readSchema(final AnnotationScannerContext context, SchemaSupport.setType(schema, readSchemaType(context, annotation, schema, defaults)); Object example = parseSchemaAttr(context, annotation, SchemaConstant.PROP_EXAMPLE, defaults, type); + List examples = SchemaFactory.> readAttr(context, annotation, SchemaConstant.PROP_EXAMPLES, egs -> Arrays.stream(egs) .map(e -> parseValue(context, e, type)) .collect(Collectors.toCollection(ArrayList::new)), - defaults); - if (examples != null) { - if (example != null) { - examples.add(example); + /* + * Do not allow default examples to be added if the annotation has specified an example + * with the deprecated property. + */ + example != null ? Collections.emptyMap() : defaults); + + if (context.getConfig().mergeSchemaExamples()) { + if (examples != null) { + if (example != null) { + examples.add(example); + } + schema.setExamples(examples); + } else { + schema.setExamples(wrapInList(example)); } - schema.setExamples(examples); } else { - schema.setExamples(wrapInList(example)); + schema.setExample(example); + schema.setExamples(examples); } schema.setDefaultValue( @@ -579,7 +590,7 @@ public static Schema typeToSchema(final AnnotationScannerContext context, Type t schema = typeToSchema(context, currentScanner.get().unwrapType(type), null); } else if (TypeUtil.isTerminalType(type)) { schema = OASFactory.createSchema(); - TypeUtil.applyTypeAttributes(type, schema); + TypeUtil.applyTypeAttributes(type, schema, schemaAnnotation); schema = schemaRegistration(context, type, schema); } else if (type.kind() == Type.Kind.ARRAY) { schema = OASFactory.createSchema().addType(SchemaType.ARRAY); @@ -647,7 +658,7 @@ public static Schema enumToSchema(final AnnotationScannerContext context, Type e enumSchema = readSchema(context, enumSchema, schemaAnnotation, enumKlazz, true, defaults); } else { - TypeUtil.applyTypeAttributes(enumValueType, enumSchema); + TypeUtil.applyTypeAttributes(enumValueType, enumSchema, null); 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 d6b36ba5e..c902b495e 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 @@ -182,7 +182,7 @@ public static Schema process(final AnnotationScannerContext context, Type type) */ public static Schema process(PrimitiveType primitive) { Schema primitiveSchema = OASFactory.createSchema(); - TypeUtil.applyTypeAttributes(primitive, primitiveSchema); + TypeUtil.applyTypeAttributes(primitive, primitiveSchema, null); return primitiveSchema; } @@ -197,7 +197,7 @@ Schema process() { // If top level item is simple if (TypeUtil.isTerminalType(rootClassType)) { Schema simpleSchema = OASFactory.createSchema(); - TypeUtil.applyTypeAttributes(rootClassType, simpleSchema); + TypeUtil.applyTypeAttributes(rootClassType, simpleSchema, null); return simpleSchema; } diff --git a/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/AnnotationTargetProcessor.java b/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/AnnotationTargetProcessor.java index 3d8c7a593..0ca0518c0 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/AnnotationTargetProcessor.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/AnnotationTargetProcessor.java @@ -153,7 +153,7 @@ Schema processField() { // Set any default values that apply to the type schema as a result of the TypeProcessor if (!TypeUtil.isTypeOverridden(context, fieldType, schemaAnnotation)) { - TypeUtil.applyTypeAttributes(fieldType, initTypeSchema); + TypeUtil.applyTypeAttributes(fieldType, initTypeSchema, schemaAnnotation); } // The registeredTypeSchema will be a reference to typeSchema if registration occurs 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 e14dbcbac..6155bebad 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 @@ -176,7 +176,7 @@ private Type readArrayType(ArrayType arrayType, Schema arraySchema) { } // Only use component (excludes the special name formatting for arrays). - TypeUtil.applyTypeAttributes(componentType, itemSchema); + TypeUtil.applyTypeAttributes(componentType, itemSchema, null); if (!isTerminalType(componentType) && index.containsClass(componentType)) { // If it's not a terminal type, then push for later inspection. @@ -290,7 +290,7 @@ private Schema readGenericValueType(Type valueType) { Schema valueSchema = OASFactory.createSchema(); if (isTerminalType(valueType)) { - TypeUtil.applyTypeAttributes(valueType, valueSchema); + TypeUtil.applyTypeAttributes(valueType, valueSchema, null); } else if (valueType.kind() == Kind.PARAMETERIZED_TYPE) { readParameterizedType(valueType.asParameterizedType(), valueSchema); } else { @@ -346,7 +346,7 @@ private Type resolveTypeVariable(Schema schema, Type fieldType, boolean pushToSt if (isTerminalType(resolvedType) || !index.containsClass(resolvedType)) { DataObjectLogging.logger.terminalType(resolvedType); - TypeUtil.applyTypeAttributes(resolvedType, schema); + TypeUtil.applyTypeAttributes(resolvedType, schema, null); } else if (pushToStack) { // Add resolved type to stack. pushToStack(resolvedType, schema); diff --git a/core/src/main/java/io/smallrye/openapi/runtime/util/TypeUtil.java b/core/src/main/java/io/smallrye/openapi/runtime/util/TypeUtil.java index 454dd5a1c..7ed7d583c 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/util/TypeUtil.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/util/TypeUtil.java @@ -413,22 +413,17 @@ public static boolean isTypeOverridden(AnnotationScannerContext context, Type cl * @param classType the type * @param schema a writable schema to be updated with the type's default schema attributes */ - public static void applyTypeAttributes(Type classType, Schema schema) { + @SuppressWarnings("unchecked") + public static void applyTypeAttributes(Type classType, Schema schema, AnnotationInstance schemaAnnotation) { Map properties = getTypeAttributes(classType); SchemaSupport.setType(schema, (SchemaType) properties.get(SchemaConstant.PROP_TYPE)); schema.setFormat((String) properties.get(SchemaConstant.PROP_FORMAT)); schema.setPattern((String) properties.get(SchemaConstant.PROP_PATTERN)); - schema.setExamples(wrapInList(properties.get(SchemaConstant.PROP_EXAMPLE))); - schema.setExternalDocs((ExternalDocumentation) properties.get(SchemaConstant.PROP_EXTERNAL_DOCS)); - } - - private static List wrapInList(E item) { - if (item == null) { - return null; - } else { - return Collections.singletonList(item); + if (schemaAnnotation == null || schemaAnnotation.value(SchemaConstant.PROP_EXAMPLE) == null) { + schema.setExamples((List) properties.get(SchemaConstant.PROP_EXAMPLES)); } + schema.setExternalDocs((ExternalDocumentation) properties.get(SchemaConstant.PROP_EXTERNAL_DOCS)); } /** @@ -837,7 +832,7 @@ Builder pattern(String pattern) { } Builder example(Object example) { - properties.put(SchemaConstant.PROP_EXAMPLE, example); + properties.put(SchemaConstant.PROP_EXAMPLES, Collections.singletonList(example)); return this; } diff --git a/core/src/test/java/io/smallrye/openapi/runtime/scanner/StandaloneSchemaScanTest.java b/core/src/test/java/io/smallrye/openapi/runtime/scanner/StandaloneSchemaScanTest.java index cca4618e1..08f193b3a 100644 --- a/core/src/test/java/io/smallrye/openapi/runtime/scanner/StandaloneSchemaScanTest.java +++ b/core/src/test/java/io/smallrye/openapi/runtime/scanner/StandaloneSchemaScanTest.java @@ -6,6 +6,7 @@ import java.io.IOException; import java.lang.annotation.ElementType; import java.lang.annotation.Target; +import java.time.LocalTime; import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Collection; @@ -766,6 +767,7 @@ public String getWo() { @Test void testExceptionalExampleParsing() throws IOException, JSONException { + // All example properties convert to `examples` in the model by default @Schema(name = "Bean") class Bean { @Schema(example = "{ Looks like object, but invalid }") @@ -820,4 +822,23 @@ public void getProperty2(java.net.URL getProperty2) { assertJsonEquals("components.schemas.javabean-property-prefix.json", Bean.class, java.net.URL.class); } + + @Test + void testExampleNotMerged() throws IOException, JSONException { + @Schema(name = "Bean") + class DTO { + + @Schema(example = "Hello World") // NOSONAR + String name; + + @Schema(example = "14:45:30.987654321") // NOSONAR + LocalTime localTime; + + @Schema(example = "14:45:30.999999999", nullable = true) // NOSONAR + LocalTime localTimeNullable; + } + + assertJsonEquals("components.schemas.example-not-merged.json", + scan(config(SmallRyeOASConfig.SMALLRYE_MERGE_SCHEMA_EXAMPLES, "false"), null, new Class[] { DTO.class })); + } } diff --git a/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.example-not-merged.json b/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.example-not-merged.json new file mode 100644 index 000000000..1fed995f3 --- /dev/null +++ b/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.example-not-merged.json @@ -0,0 +1,34 @@ +{ + "openapi" : "3.1.0", + "components" : { + "schemas" : { + "Bean" : { + "type" : "object", + "properties" : { + "name" : { + "type" : "string", + "example" : "Hello World" + }, + "localTime" : { + "type" : "string", + "format" : "local-time", + "externalDocs" : { + "description" : "As defined by 'partial-time' in RFC3339", + "url" : "https://www.rfc-editor.org/rfc/rfc3339.html#section-5.6" + }, + "example" : "14:45:30.987654321" + }, + "localTimeNullable" : { + "type" : [ "string", "null" ], + "format" : "local-time", + "example" : "14:45:30.999999999", + "externalDocs" : { + "description" : "As defined by 'partial-time' in RFC3339", + "url" : "https://www.rfc-editor.org/rfc/rfc3339.html#section-5.6" + } + } + } + } + } + } +}