diff --git a/.github/ISSUE_TEMPLATE/01_bug_report.md b/.github/ISSUE_TEMPLATE/01_bug_report.md new file mode 100644 index 0000000000..87df8b5cca --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01_bug_report.md @@ -0,0 +1,59 @@ +--- +name: Bug Report +about: Report an issue in swagger-core +title: "[Bug]: " +labels: Bug +assignees: '' +--- + +## Description of the problem/issue + + + +## Affected Version + + +e.g. 2.2.21 + + +Earliest version the bug appears in (if known): +e.g. 2.2.17 + +## Steps to Reproduce + + + +1. ... +2. ... +3. ... + +## Expected Behavior + + + +## Actual Behavior + + + +## Logs / Stack Traces + + + +## Additional Context + + + +## Checklist + +- [ ] I have searched the [existing issues](https://github.com/swagger-api/swagger-core/issues) and this is not a duplicate. +- [ ] I have provided sufficient information for maintainers to reproduce the issue. diff --git a/.github/ISSUE_TEMPLATE/02_question.md b/.github/ISSUE_TEMPLATE/02_question.md new file mode 100644 index 0000000000..45f6b5c21f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/02_question.md @@ -0,0 +1,43 @@ +--- +name: Question +about: Ask a question about swagger-core usage or behavior +title: "[Question]: " +labels: Question +assignees: '' +--- + +## Question + + + +## Affected Version + + +e.g. 2.2.21 + +## Context + + + +```java +// Example code snippet here +``` + +## Additional Details + + + +## Checklist + +- [ ] I have searched the [existing issues](https://github.com/swagger-api/swagger-core/issues) and documentation before asking. +- [ ] I have provided enough information for others to understand my question. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/03_feature_request.md b/.github/ISSUE_TEMPLATE/03_feature_request.md new file mode 100644 index 0000000000..9450a7b501 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/03_feature_request.md @@ -0,0 +1,50 @@ +--- +name: Feature Request +about: Suggest a new feature or enhancement for swagger-core +title: "[Feature]: " +labels: Feature +assignees: '' +--- + +## Feature Description + + + +## Use Case + + + +## Suggested Solution (optional) + + + +## Alternatives Considered + + + +## Additional Context + + + +## Checklist + +- [ ] I have searched the [existing issues](https://github.com/swagger-api/swagger-core/issues) to ensure this is not a duplicate. +- [ ] This feature would be useful to more than just my use case. +- [ ] I have provided enough detail for the maintainers to understand the scope of the request. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000..d8d9a24e9d --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,43 @@ +# Pull Request + +Thank you for contributing to **swagger-core**! + +Please fill out the following information to help us review your PR efficiently. + +--- + +## Description + + + +Fixes: + +## Type of Change + + + +- [ ] ๐Ÿ› Bug fix +- [ ] โœจ New feature +- [ ] โ™ป๏ธ Refactor (non-breaking change) +- [ ] ๐Ÿงช Tests +- [ ] ๐Ÿ“ Documentation +- [ ] ๐Ÿงน Chore (build or tooling) + +## Checklist + + + +- [ ] I have added/updated tests as needed +- [ ] I have added/updated documentation where applicable +- [ ] The PR title is descriptive +- [ ] The code builds and passes tests locally +- [ ] I have linked related issues (if any) + +## Screenshots / Additional Context + + \ No newline at end of file diff --git a/README.md b/README.md index 71f9b737cd..ce9f733d77 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,13 @@ The OpenAPI Specification has undergone several revisions since initial creation Swagger core Version | Release Date | OpenAPI Spec compatibility | Notes | Status ------------------------- | ------------ | -------------------------- | ----- | ---- -2.2.33 (**current stable**)| 2025-06-12 | 3.x | [tag v2.2.33](https://github.com/swagger-api/swagger-core/tree/v2.2.33) | Supported +2.2.39 (**current stable**)| 2025-10-13 | 3.x | [tag v2.2.39](https://github.com/swagger-api/swagger-core/tree/v2.2.39) | Supported +2.2.38 | 2025-09-29 | 3.x | [tag v2.2.38](https://github.com/swagger-api/swagger-core/tree/v2.2.38) | Supported +2.2.37 | 2025-09-16 | 3.x | [tag v2.2.37](https://github.com/swagger-api/swagger-core/tree/v2.2.37) | Supported +2.2.36 | 2025-08-18 | 3.x | [tag v2.2.36](https://github.com/swagger-api/swagger-core/tree/v2.2.36) | Supported +2.2.35 | 2025-07-31 | 3.x | [tag v2.2.35](https://github.com/swagger-api/swagger-core/tree/v2.2.35) | Supported +2.2.34 | 2025-06-20 | 3.x | [tag v2.2.34](https://github.com/swagger-api/swagger-core/tree/v2.2.34) | Supported +2.2.33 | 2025-06-12 | 3.x | [tag v2.2.33](https://github.com/swagger-api/swagger-core/tree/v2.2.33) | Supported 2.2.32 | 2025-05-14 | 3.x | [tag v2.2.32](https://github.com/swagger-api/swagger-core/tree/v2.2.32) | Supported 2.2.31 | 2025-05-13 | 3.x | [tag v2.2.31](https://github.com/swagger-api/swagger-core/tree/v2.2.31) | Supported 2.2.30 | 2025-04-07 | 3.x | [tag v2.2.30](https://github.com/swagger-api/swagger-core/tree/v2.2.30) | Supported @@ -126,7 +132,7 @@ You need the following installed and available in your $PATH: * Jackson 2.4.5 or greater -### To build from source (currently 2.2.34-SNAPSHOT) +### To build from source (currently 2.2.40-SNAPSHOT) ``` # first time building locally mvn -N diff --git a/modules/swagger-annotations/pom.xml b/modules/swagger-annotations/pom.xml index b1771bd99d..14fbb1a6f9 100644 --- a/modules/swagger-annotations/pom.xml +++ b/modules/swagger-annotations/pom.xml @@ -3,7 +3,7 @@ io.swagger.core.v3 swagger-project - 2.2.34-SNAPSHOT + 2.2.40-SNAPSHOT ../.. 4.0.0 diff --git a/modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/media/Schema.java b/modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/media/Schema.java index 1271ee3fd6..fb3c90fe81 100644 --- a/modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/media/Schema.java +++ b/modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/media/Schema.java @@ -176,8 +176,11 @@ /** * Allows to specify the required mode (RequiredMode.AUTO, REQUIRED, NOT_REQUIRED) - * - * RequiredMode.AUTO: will let the library decide based on its heuristics. + * RequiredMode.AUTO: the library decides using heuristics: + * - Bean Validation / nullability annotations (@NotNull, @NonNull, @NotBlank, @NotEmpty) - required + * - Optional - not required + * - Primitive types (int, boolean, etc.) - not required unless annotated + * - Other object fields without any constraints - not required * RequiredMode.REQUIRED: will force the item to be considered as required regardless of heuristics. * RequiredMode.NOT_REQUIRED: will force the item to be considered as not required regardless of heuristics. * diff --git a/modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/responses/FailedApiResponse.java b/modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/responses/FailedApiResponse.java new file mode 100644 index 0000000000..59be127026 --- /dev/null +++ b/modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/responses/FailedApiResponse.java @@ -0,0 +1,57 @@ +package io.swagger.v3.oas.annotations.responses; + +import io.swagger.v3.oas.annotations.extensions.Extension; +import io.swagger.v3.oas.annotations.media.Content; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A meta-annotation that bundles common error response definitions for API operations. + *

+ * Includes default definitions for: + *

+ * Can be used at type level to apply to all operations in a controller, + * or at method level for individual operations. + * + * @see ApiResponse + * @see ApiResponses + * @since 2.2.32 + */ + +@Target({ElementType.METHOD, ElementType.TYPE, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@ApiResponse(responseCode = "400", description = "Bad Request", content = @Content) +@ApiResponse(responseCode = "401", description = "Unauthorized", content = @Content) +@ApiResponse(responseCode = "403", description = "Forbidden", content = @Content) +@ApiResponse(responseCode = "404", description = "Not Found", content = @Content) +@ApiResponse(responseCode = "429", description = "Too Many Requests", content = @Content) +@ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content) +@ApiResponse(responseCode = "503", description = "Service Unavailable", content = @Content) +public @interface FailedApiResponse { + /** + * A reference to a response definition in components responses. + * + * @return the reference + * @since 2.2.32 + */ + String ref() default ""; + + /** + * The list of optional extensions. + * + * @return an optional array of extensions + * @since 2.2.32 + */ + Extension[] extensions() default {}; +} diff --git a/modules/swagger-core/pom.xml b/modules/swagger-core/pom.xml index fa0adfdcdc..9c69430078 100644 --- a/modules/swagger-core/pom.xml +++ b/modules/swagger-core/pom.xml @@ -3,7 +3,7 @@ io.swagger.core.v3 swagger-project - 2.2.34-SNAPSHOT + 2.2.40-SNAPSHOT ../.. 4.0.0 diff --git a/modules/swagger-core/src/main/java/io/swagger/v3/core/converter/AnnotatedType.java b/modules/swagger-core/src/main/java/io/swagger/v3/core/converter/AnnotatedType.java index da0b642f0d..e9a252fcd8 100644 --- a/modules/swagger-core/src/main/java/io/swagger/v3/core/converter/AnnotatedType.java +++ b/modules/swagger-core/src/main/java/io/swagger/v3/core/converter/AnnotatedType.java @@ -8,9 +8,11 @@ import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; +import java.util.Comparator; import java.util.List; import java.util.Objects; import java.util.function.Function; +import java.util.stream.Collectors; public class AnnotatedType { private Type type; @@ -21,6 +23,7 @@ public class AnnotatedType { private boolean schemaProperty; private Annotation[] ctxAnnotations; private boolean resolveAsRef; + private boolean resolveEnumAsRef; private JsonView jsonViewAnnotation; private boolean includePropertiesWithoutJSONView = true; private boolean skipSchemaName; @@ -88,6 +91,19 @@ public AnnotatedType resolveAsRef(boolean resolveAsRef) { return this; } + public boolean isResolveEnumAsRef() { + return resolveEnumAsRef; + } + + public void setResolveEnumAsRef(boolean resolveEnumAsRef) { + this.resolveEnumAsRef = resolveEnumAsRef; + } + + public AnnotatedType resolveEnumAsRef(boolean resolveEnumAsRef) { + this.resolveEnumAsRef = resolveEnumAsRef; + return this; + } + public boolean isSchemaProperty() { return schemaProperty; } @@ -141,11 +157,11 @@ public AnnotatedType name(String name) { } public Annotation[] getCtxAnnotations() { - return ctxAnnotations; + return ctxAnnotations == null ? null : Arrays.copyOf(ctxAnnotations, ctxAnnotations.length); } public void setCtxAnnotations(Annotation[] ctxAnnotations) { - this.ctxAnnotations = ctxAnnotations; + this.ctxAnnotations = ctxAnnotations == null ? null : Arrays.copyOf(ctxAnnotations, ctxAnnotations.length); } public AnnotatedType ctxAnnotations(Annotation[] ctxAnnotations) { @@ -227,49 +243,35 @@ public AnnotatedType propertyName(String propertyName) { return this; } + private List getProcessedAnnotations(Annotation[] annotations) { + if (annotations == null || annotations.length == 0) { + return new ArrayList<>(); + } + return Arrays.stream(annotations) + .filter(a -> { + String pkg = a.annotationType().getPackage().getName(); + return !pkg.startsWith("java.") && !pkg.startsWith("jdk.") && !pkg.startsWith("sun."); + }) + .sorted(Comparator.comparing(a -> a.annotationType().getName())) + .collect(Collectors.toList()); + } + @Override public boolean equals(Object o) { - if (this == o) { - return true; - } - if (!(o instanceof AnnotatedType)) { - return false; - } + if (this == o) return true; + if (!(o instanceof AnnotatedType)) return false; AnnotatedType that = (AnnotatedType) o; - - if ((type == null && that.type != null) || (type != null && that.type == null)) { - return false; - } - - if (type != null && that.type != null && !type.equals(that.type)) { - return false; - } - return Arrays.equals(this.ctxAnnotations, that.ctxAnnotations); + List thisAnnotatinons = getProcessedAnnotations(this.ctxAnnotations); + List thatAnnotatinons = getProcessedAnnotations(that.ctxAnnotations); + return includePropertiesWithoutJSONView == that.includePropertiesWithoutJSONView && + Objects.equals(type, that.type) && + Objects.equals(thisAnnotatinons, thatAnnotatinons) && + Objects.equals(jsonViewAnnotation, that.jsonViewAnnotation); } - @Override public int hashCode() { - if (ctxAnnotations == null || ctxAnnotations.length == 0) { - return Objects.hash(type, "fixed"); - } - List meaningfulAnnotations = new ArrayList<>(); - - boolean hasDifference = false; - for (Annotation a: ctxAnnotations) { - if(!a.annotationType().getName().startsWith("sun") && !a.annotationType().getName().startsWith("jdk")) { - meaningfulAnnotations.add(a); - } else { - hasDifference = true; - } - } - int result = 1; - result = 31 * result + (type == null ? 0 : Objects.hash(type, "fixed")); - if (hasDifference) { - result = 31 * result + meaningfulAnnotations.hashCode(); - } else { - result = 31 * result + Arrays.hashCode(ctxAnnotations); - } - return result; + List processedAnnotations = getProcessedAnnotations(this.ctxAnnotations); + return Objects.hash(type, jsonViewAnnotation, includePropertiesWithoutJSONView, processedAnnotations); } } diff --git a/modules/swagger-core/src/main/java/io/swagger/v3/core/jackson/ModelResolver.java b/modules/swagger-core/src/main/java/io/swagger/v3/core/jackson/ModelResolver.java index 98bcb4ddeb..7c00931b23 100644 --- a/modules/swagger-core/src/main/java/io/swagger/v3/core/jackson/ModelResolver.java +++ b/modules/swagger-core/src/main/java/io/swagger/v3/core/jackson/ModelResolver.java @@ -391,7 +391,7 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context } List> composedSchemaReferencedClasses = getComposedSchemaReferencedClasses(type.getRawClass(), annotatedType.getCtxAnnotations(), resolvedSchemaAnnotation); - boolean isComposedSchema = composedSchemaReferencedClasses != null; + boolean hasCompositionKeywords = composedSchemaReferencedClasses != null; if (isPrimitive) { XML xml = resolveXml(beanDesc.getClassInfo(), annotatedType.getCtxAnnotations(), resolvedSchemaAnnotation); @@ -411,14 +411,14 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context schema.setItems(model); return schema; } - if (type.isEnumType() && shouldResolveEnumAsRef(resolvedSchemaAnnotation)) { + if (type.isEnumType() && shouldResolveEnumAsRef(resolvedSchemaAnnotation, annotatedType.isResolveEnumAsRef())) { // Store off the ref and add the enum as a top-level model context.defineModel(name, model, annotatedType, null); // Return the model as a ref only property model = openapi31 ? new JsonSchema() : new Schema(); model.$ref(Components.COMPONENTS_SCHEMAS_REF + name); } - if (!isComposedSchema) { + if (!hasCompositionKeywords) { if (schemaRefFromAnnotation != null && model != null) { model.raw$ref(schemaRefFromAnnotation); } @@ -464,7 +464,7 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context if (type.isContainerType()) { // TODO currently a MapSchema or ArraySchema don't also support composed schema props (oneOf,..) - isComposedSchema = false; + hasCompositionKeywords = false; JavaType keyType = type.getKeyType(); JavaType valueType = type.getContentType(); String pName = null; @@ -540,6 +540,7 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context .propertyName(annotatedType.getPropertyName()) .jsonViewAnnotation(annotatedType.getJsonViewAnnotation()) .components(annotatedType.getComponents()) + .resolveEnumAsRef(annotatedType.isResolveEnumAsRef()) .parent(annotatedType.getParent())); if (items == null) { @@ -593,7 +594,7 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context return null; } } - } else if (isComposedSchema) { + } else if (hasCompositionKeywords) { model = openapi31 ? new JsonSchema() : new ComposedSchema(); model.name(name); if ( @@ -750,10 +751,7 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context Annotation[] ctxAnnotation31 = null; Schema.SchemaResolution resolvedSchemaResolution = AnnotationsUtils.resolveSchemaResolution(this.schemaResolution, ctxSchema); - if ( - Schema.SchemaResolution.ALL_OF.equals(resolvedSchemaResolution) || - Schema.SchemaResolution.ALL_OF_REF.equals(resolvedSchemaResolution) || - openapi31) { + if (AnnotationsUtils.areSiblingsAllowed(resolvedSchemaResolution, openapi31)) { List ctxAnnotations31List = new ArrayList<>(); if (annotations != null) { for (Annotation a : annotations) { @@ -762,9 +760,6 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context !(a instanceof io.swagger.v3.oas.annotations.media.ArraySchema)) { ctxAnnotations31List.add(a); } - if ((ctxSchema != null) && (!ctxSchema.implementation().equals(Void.class) || StringUtils.isNotEmpty(ctxSchema.type()))) { - ctxAnnotations31List.add(a); - } } ctxAnnotation31 = ctxAnnotations31List.toArray(new Annotation[ctxAnnotations31List.size()]); } @@ -794,7 +789,8 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context .skipSchemaName(true) .schemaProperty(true) .components(annotatedType.getComponents()) - .propertyName(propName); + .propertyName(propName) + .resolveEnumAsRef(AnnotationsUtils.computeEnumAsRef(ctxSchema, ctxArraySchema)); if ( Schema.SchemaResolution.ALL_OF.equals(resolvedSchemaResolution) || Schema.SchemaResolution.ALL_OF_REF.equals(resolvedSchemaResolution) || @@ -821,6 +817,9 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context return openapi31 ? new JsonSchema() : new Schema(); } }); + + boolean areSiblingsAllowed = AnnotationsUtils.areSiblingsAllowed(resolvedSchemaResolution, openapi31); + aType = AnnotationsUtils.addTypeWhenSiblingsAllowed(aType, ctxSchema, areSiblingsAllowed); property = context.resolve(aType); property = clone(property); Schema ctxProperty = null; @@ -1041,9 +1040,9 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context } } - if (isComposedSchema) { + if (hasCompositionKeywords) { - ComposedSchema composedSchema = (ComposedSchema) model; + Schema schemaWithCompositionKeys = model; Class[] allOf = resolvedSchemaAnnotation.allOf(); Class[] anyOf = resolvedSchemaAnnotation.anyOf(); @@ -1061,12 +1060,12 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context refSchema = allOfRef; } // allOf could have already being added during subtype resolving - if (composedSchema.getAllOf() == null || !composedSchema.getAllOf().contains(refSchema)) { - composedSchema.addAllOfItem(refSchema); + if (schemaWithCompositionKeys.getAllOf() == null || !schemaWithCompositionKeys.getAllOf().contains(refSchema)) { + schemaWithCompositionKeys.addAllOfItem(refSchema); } // remove shared properties defined in the parent if (isSubtype(beanDesc.getClassInfo(), c)) { - removeParentProperties(composedSchema, allOfRef); + removeParentProperties(schemaWithCompositionKeys, allOfRef); } }); @@ -1079,14 +1078,14 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context Schema anyOfRef = context.resolve(new AnnotatedType().components(annotatedType.getComponents()).type(c).jsonViewAnnotation(annotatedType.getJsonViewAnnotation())); if (anyOfRef != null) { if (StringUtils.isNotBlank(anyOfRef.getName())) { - composedSchema.addAnyOfItem(new Schema().$ref(Components.COMPONENTS_SCHEMAS_REF + anyOfRef.getName())); + schemaWithCompositionKeys.addAnyOfItem(new Schema().$ref(Components.COMPONENTS_SCHEMAS_REF + anyOfRef.getName())); } else { - composedSchema.addAnyOfItem(anyOfRef); + schemaWithCompositionKeys.addAnyOfItem(anyOfRef); } } // remove shared properties defined in the parent if (isSubtype(beanDesc.getClassInfo(), c)) { - removeParentProperties(composedSchema, anyOfRef); + removeParentProperties(schemaWithCompositionKeys, anyOfRef); } }); @@ -1100,25 +1099,26 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context Schema oneOfRef = context.resolve(new AnnotatedType().components(annotatedType.getComponents()).type(c).jsonViewAnnotation(annotatedType.getJsonViewAnnotation())); if (oneOfRef != null) { if (StringUtils.isBlank(oneOfRef.getName())) { - composedSchema.addOneOfItem(oneOfRef); + schemaWithCompositionKeys.addOneOfItem(oneOfRef); } else { - composedSchema.addOneOfItem(new Schema().$ref(Components.COMPONENTS_SCHEMAS_REF + oneOfRef.getName())); + schemaWithCompositionKeys.addOneOfItem(new Schema().$ref(Components.COMPONENTS_SCHEMAS_REF + oneOfRef.getName())); } // remove shared properties defined in the parent if (isSubtype(beanDesc.getClassInfo(), c)) { - removeParentProperties(composedSchema, oneOfRef); + removeParentProperties(schemaWithCompositionKeys, oneOfRef); } } + dropRootRefIfComposed(schemaWithCompositionKeys); }); if (!composedModelPropertiesAsSibling) { - if (composedSchema.getAllOf() != null && !composedSchema.getAllOf().isEmpty()) { - if (composedSchema.getProperties() != null && !composedSchema.getProperties().isEmpty()) { + if (schemaWithCompositionKeys.getAllOf() != null && !schemaWithCompositionKeys.getAllOf().isEmpty()) { + if (schemaWithCompositionKeys.getProperties() != null && !schemaWithCompositionKeys.getProperties().isEmpty()) { Schema propSchema = openapi31 ? new JsonSchema().typesItem("object") : new ObjectSchema(); - propSchema.properties(composedSchema.getProperties()); - composedSchema.setProperties(null); - composedSchema.addAllOfItem(propSchema); + propSchema.properties(schemaWithCompositionKeys.getProperties()); + schemaWithCompositionKeys.setProperties(null); + schemaWithCompositionKeys.addAllOfItem(propSchema); } } } @@ -1139,7 +1139,7 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context Schema.SchemaResolution resolvedSchemaResolution = AnnotationsUtils.resolveSchemaResolution(this.schemaResolution, resolvedSchemaAnnotation); if (model != null && annotatedType.isResolveAsRef() && - (isComposedSchema || isObjectSchema(model) || implicitObject) && + (hasCompositionKeywords || isObjectSchema(model) || implicitObject) && StringUtils.isNotBlank(model.getName())) { if (context.getDefinedModels().containsKey(model.getName())) { if (!Schema.SchemaResolution.INLINE.equals(resolvedSchemaResolution)) { @@ -1209,6 +1209,38 @@ private Stream getRecordComponentAnnotations(BeanPropertyDefinition } } + private void dropRootRefIfComposed(Schema s) { + if (s == null || s.get$ref() == null) { + return; + } + + if (!isComposedSchema(s)) { + return; + } + + String ref = s.get$ref(); + if (refMatchesAnyComposedItem(s, ref)) { + s.set$ref(null); + } + } + + private boolean isComposedSchema(Schema s) { + return (s.getOneOf() != null && !s.getOneOf().isEmpty()) + || (s.getAnyOf() != null && !s.getAnyOf().isEmpty()) + || (s.getAllOf() != null && !s.getAllOf().isEmpty()); + } + + private boolean refMatchesAnyComposedItem(Schema s, String ref) { + return refMatchesInList(s.getOneOf(), ref) + || refMatchesInList(s.getAllOf(), ref) + || refMatchesInList(s.getAnyOf(), ref); + } + + private boolean refMatchesInList(List schemas, String ref) { + return schemas != null && schemas.stream() + .anyMatch(schema -> ref.equals(schema.get$ref())); + } + private Boolean isRecordType(BeanPropertyDefinition propDef) { try { if (propDef.getPrimaryMember() != null) { @@ -1239,8 +1271,8 @@ private Stream getGenericTypeArgumentAnnotations(java.lang.reflect.A .orElseGet(Stream::of); } - private boolean shouldResolveEnumAsRef(io.swagger.v3.oas.annotations.media.Schema resolvedSchemaAnnotation) { - return (resolvedSchemaAnnotation != null && resolvedSchemaAnnotation.enumAsRef()) || ModelResolver.enumsAsRef; + private boolean shouldResolveEnumAsRef(io.swagger.v3.oas.annotations.media.Schema resolvedSchemaAnnotation, boolean isResolveEnumAsRef) { + return (resolvedSchemaAnnotation != null && resolvedSchemaAnnotation.enumAsRef()) || ModelResolver.enumsAsRef || isResolveEnumAsRef; } protected Type findJsonValueType(final BeanDescription beanDesc) { @@ -2093,43 +2125,7 @@ private boolean resolveSubtypes(Schema model, BeanDescription bean, ModelConvert ComposedSchema composedSchema = null; if (!(subtypeModel instanceof ComposedSchema)) { // create composed schema - // TODO #2312 - smarter way needs clone implemented in #2227 - composedSchema = (ComposedSchema) new ComposedSchema() - .title(subtypeModel.getTitle()) - .name(subtypeModel.getName()) - .deprecated(subtypeModel.getDeprecated()) - .additionalProperties(subtypeModel.getAdditionalProperties()) - .description(subtypeModel.getDescription()) - .discriminator(subtypeModel.getDiscriminator()) - .exclusiveMaximum(subtypeModel.getExclusiveMaximum()) - .exclusiveMinimum(subtypeModel.getExclusiveMinimum()) - .externalDocs(subtypeModel.getExternalDocs()) - .format(subtypeModel.getFormat()) - .maximum(subtypeModel.getMaximum()) - .maxItems(subtypeModel.getMaxItems()) - .maxLength(subtypeModel.getMaxLength()) - .maxProperties(subtypeModel.getMaxProperties()) - .minimum(subtypeModel.getMinimum()) - .minItems(subtypeModel.getMinItems()) - .minLength(subtypeModel.getMinLength()) - .minProperties(subtypeModel.getMinProperties()) - .multipleOf(subtypeModel.getMultipleOf()) - .not(subtypeModel.getNot()) - .nullable(subtypeModel.getNullable()) - .pattern(subtypeModel.getPattern()) - .properties(subtypeModel.getProperties()) - .readOnly(subtypeModel.getReadOnly()) - .required(subtypeModel.getRequired()) - .type(subtypeModel.getType()) - .uniqueItems(subtypeModel.getUniqueItems()) - .writeOnly(subtypeModel.getWriteOnly()) - .xml(subtypeModel.getXml()) - .extensions(subtypeModel.getExtensions()); - - if (subtypeModel.getExample() != null || subtypeModel.getExampleSetFlag()) { - composedSchema.example(subtypeModel.getExample()); - } - composedSchema.setEnum(subtypeModel.getEnum()); + composedSchema = ComposedSchema.from(subtypeModel); } else { composedSchema = (ComposedSchema) subtypeModel; } diff --git a/modules/swagger-core/src/main/java/io/swagger/v3/core/util/AnnotationsUtils.java b/modules/swagger-core/src/main/java/io/swagger/v3/core/util/AnnotationsUtils.java index 761bc1b7f7..e331732722 100644 --- a/modules/swagger-core/src/main/java/io/swagger/v3/core/util/AnnotationsUtils.java +++ b/modules/swagger-core/src/main/java/io/swagger/v3/core/util/AnnotationsUtils.java @@ -548,12 +548,21 @@ public static Optional getArraySchema(io.swagger.v3.oas.annotations.medi if (arraySchema.schema() != null) { if (arraySchema.schema().implementation().equals(Void.class)) { - getSchemaFromAnnotation(arraySchema.schema(), components, jsonViewAnnotation, openapi31).ifPresent(arraySchemaObject::setItems); + getSchemaFromAnnotation(arraySchema.schema(), components, jsonViewAnnotation, openapi31, arraySchemaObject.getItems()).ifPresent(arraySchemaObject::setItems); } else if (processSchemaImplementation) { getSchema(arraySchema.schema(), arraySchema, false, arraySchema.schema().implementation(), components, jsonViewAnnotation, openapi31).ifPresent(arraySchemaObject::setItems); } } + if (arraySchema.arraySchema() != null) { + if (StringUtils.isNotBlank(arraySchema.arraySchema().description())) { + arraySchemaObject.setDescription(arraySchema.arraySchema().description()); + } + if (StringUtils.isNotBlank(arraySchema.arraySchema().title())) { + arraySchemaObject.setTitle(arraySchema.arraySchema().title()); + } + } + return Optional.of(arraySchemaObject); } @@ -2907,4 +2916,28 @@ public static Schema.SchemaResolution resolveSchemaResolution(Schema.SchemaResol } return globalSchemaResolution; } + + public static boolean computeEnumAsRef(io.swagger.v3.oas.annotations.media.Schema ctxSchema, io.swagger.v3.oas.annotations.media.ArraySchema ctxArraySchema) { + if (ctxSchema != null && ctxSchema.enumAsRef()) { + return ctxSchema.enumAsRef(); + } else if(ctxArraySchema != null && ctxArraySchema.schema() != null && ctxArraySchema.schema().enumAsRef()) { + return ctxArraySchema.schema().enumAsRef(); + } + return false; + } + + public static boolean areSiblingsAllowed(Schema.SchemaResolution resolvedSchemaResolution, boolean openapi31) { + return Schema.SchemaResolution.ALL_OF.equals(resolvedSchemaResolution) || Schema.SchemaResolution.ALL_OF_REF.equals(resolvedSchemaResolution) || openapi31; + } + + public static AnnotatedType addTypeWhenSiblingsAllowed(AnnotatedType aType, io.swagger.v3.oas.annotations.media.Schema ctxSchema, boolean areSiblingsAllowed) { + if (areSiblingsAllowed && ctxSchema != null) { + if (!Void.class.equals(ctxSchema.implementation())) { + aType.setType(ctxSchema.implementation()); + } else if (StringUtils.isNotBlank(ctxSchema.type())) { + aType.setType(ctxSchema.type().getClass()); + } + } + return aType; + } } diff --git a/modules/swagger-core/src/main/java/io/swagger/v3/core/util/Json.java b/modules/swagger-core/src/main/java/io/swagger/v3/core/util/Json.java index 899c7476ed..055d31a4eb 100644 --- a/modules/swagger-core/src/main/java/io/swagger/v3/core/util/Json.java +++ b/modules/swagger-core/src/main/java/io/swagger/v3/core/util/Json.java @@ -3,6 +3,8 @@ import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class Json { @@ -10,6 +12,8 @@ private static final class ObjectMapperHolder { private static final ObjectMapper MAPPER = ObjectMapperFactory.createJson(); } + private static final Logger LOGGER = LoggerFactory.getLogger(Json.class); + public static ObjectMapper mapper() { return ObjectMapperHolder.MAPPER; } @@ -22,16 +26,17 @@ public static String pretty(Object o) { try { return pretty().writeValueAsString(o); } catch (Exception e) { - e.printStackTrace(); + PrettyPrintHelper.emitError(LOGGER, "Error serializing object to JSON", e); return null; } } public static void prettyPrint(Object o) { try { - System.out.println(pretty().writeValueAsString(o).replace("\r", "")); + String prettyString = pretty().writeValueAsString(o).replace("\r", ""); + PrettyPrintHelper.emit(LOGGER, prettyString); } catch (Exception e) { - e.printStackTrace(); + PrettyPrintHelper.emitError(LOGGER, "Error pretty-printing JSON", e); } } } diff --git a/modules/swagger-core/src/main/java/io/swagger/v3/core/util/Json31.java b/modules/swagger-core/src/main/java/io/swagger/v3/core/util/Json31.java index 0ac52783a8..64b7600b89 100644 --- a/modules/swagger-core/src/main/java/io/swagger/v3/core/util/Json31.java +++ b/modules/swagger-core/src/main/java/io/swagger/v3/core/util/Json31.java @@ -21,7 +21,7 @@ private static final class ConverterMapperHolder { private static final ObjectMapper MAPPER = ObjectMapperFactory.createJsonConverter(); } - static Logger LOGGER = LoggerFactory.getLogger(Json31.class); + private static final Logger LOGGER = LoggerFactory.getLogger(Json31.class); public static ObjectMapper mapper() { return ObjectMapperHolder.MAPPER; @@ -39,16 +39,17 @@ public static String pretty(Object o) { try { return pretty().writeValueAsString(o); } catch (Exception e) { - e.printStackTrace(); + PrettyPrintHelper.emitError(LOGGER, "Error serializing object to JSON (3.1)", e); return null; } } public static void prettyPrint(Object o) { try { - System.out.println(pretty().writeValueAsString(o).replace("\r", "")); + String prettyString = pretty().writeValueAsString(o).replace("\r", ""); + PrettyPrintHelper.emit(LOGGER, prettyString); } catch (Exception e) { - e.printStackTrace(); + PrettyPrintHelper.emitError(LOGGER, "Error pretty-printing JSON (3.1)", e); } } diff --git a/modules/swagger-core/src/main/java/io/swagger/v3/core/util/PrettyPrintHelper.java b/modules/swagger-core/src/main/java/io/swagger/v3/core/util/PrettyPrintHelper.java new file mode 100644 index 0000000000..a469b1ebcd --- /dev/null +++ b/modules/swagger-core/src/main/java/io/swagger/v3/core/util/PrettyPrintHelper.java @@ -0,0 +1,48 @@ +package io.swagger.v3.core.util; + +import org.slf4j.Logger; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.function.Consumer; + +class PrettyPrintHelper { + + private static final ThreadLocal> OVERRIDE = new ThreadLocal<>(); + + private PrettyPrintHelper() { + // utility class + } + + static void setOverride(Consumer consumer) { + OVERRIDE.set(consumer); + } + + static void clearOverride() { + OVERRIDE.remove(); + } + + static void emit(Logger logger, String message) { + Consumer consumer = OVERRIDE.get(); + if (consumer != null) { + consumer.accept(message); + } else { + logger.debug(message); + } + } + + static void emitError(Logger logger, String message, Throwable throwable) { + Consumer consumer = OVERRIDE.get(); + if (consumer != null) { + StringBuilder builder = new StringBuilder(message); + if (throwable != null) { + builder.append(System.lineSeparator()); + StringWriter writer = new StringWriter(); + throwable.printStackTrace(new PrintWriter(writer)); + builder.append(writer); + } + consumer.accept(builder.toString()); + } + logger.error(message, throwable); + } +} diff --git a/modules/swagger-core/src/main/java/io/swagger/v3/core/util/Yaml.java b/modules/swagger-core/src/main/java/io/swagger/v3/core/util/Yaml.java index 2a77b70305..8a7504e1b8 100644 --- a/modules/swagger-core/src/main/java/io/swagger/v3/core/util/Yaml.java +++ b/modules/swagger-core/src/main/java/io/swagger/v3/core/util/Yaml.java @@ -3,6 +3,8 @@ import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class Yaml { @@ -10,6 +12,8 @@ private static final class ObjectMapperHolder { private static final ObjectMapper MAPPER = ObjectMapperFactory.createYaml(); } + private static final Logger LOGGER = LoggerFactory.getLogger(Yaml.class); + public static ObjectMapper mapper() { return ObjectMapperHolder.MAPPER; } @@ -22,16 +26,17 @@ public static String pretty(Object o) { try { return pretty().writeValueAsString(o); } catch (Exception e) { - e.printStackTrace(); + PrettyPrintHelper.emitError(LOGGER, "Error serializing object to YAML", e); return null; } } public static void prettyPrint(Object o) { try { - System.out.println(pretty().writeValueAsString(o)); + String prettyString = pretty().writeValueAsString(o); + PrettyPrintHelper.emit(LOGGER, prettyString); } catch (Exception e) { - e.printStackTrace(); + PrettyPrintHelper.emitError(LOGGER, "Error pretty-printing YAML", e); } } } diff --git a/modules/swagger-core/src/main/java/io/swagger/v3/core/util/Yaml31.java b/modules/swagger-core/src/main/java/io/swagger/v3/core/util/Yaml31.java index 75d9819a84..4189d4f8d9 100644 --- a/modules/swagger-core/src/main/java/io/swagger/v3/core/util/Yaml31.java +++ b/modules/swagger-core/src/main/java/io/swagger/v3/core/util/Yaml31.java @@ -17,7 +17,7 @@ private static final class ObjectMapperHolder { } - static Logger LOGGER = LoggerFactory.getLogger(Yaml31.class); + private static final Logger LOGGER = LoggerFactory.getLogger(Yaml31.class); public static ObjectMapper mapper() { return ObjectMapperHolder.MAPPER; @@ -31,16 +31,17 @@ public static String pretty(Object o) { try { return pretty().writeValueAsString(o); } catch (Exception e) { - e.printStackTrace(); + PrettyPrintHelper.emitError(LOGGER, "Error serializing object to YAML (3.1)", e); return null; } } public static void prettyPrint(Object o) { try { - System.out.println(pretty().writeValueAsString(o)); + String prettyString = pretty().writeValueAsString(o); + PrettyPrintHelper.emit(LOGGER, prettyString); } catch (Exception e) { - e.printStackTrace(); + PrettyPrintHelper.emitError(LOGGER, "Error pretty-printing YAML (3.1)", e); } } @@ -60,4 +61,5 @@ public static Map jsonSchemaAsMap(Schema schema) { LOGGER.error("Exception converting jsonSchema to Map", e); return null; } - }} + } +} diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/converting/AnnotatedTypeCachingTest.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/converting/AnnotatedTypeCachingTest.java new file mode 100644 index 0000000000..c79340ca80 --- /dev/null +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/converting/AnnotatedTypeCachingTest.java @@ -0,0 +1,72 @@ +package io.swagger.v3.core.converting; + +import io.swagger.v3.core.converter.AnnotatedType; +import io.swagger.v3.core.converter.ModelConverter; +import io.swagger.v3.core.converter.ModelConverterContext; +import io.swagger.v3.core.converter.ModelConverterContextImpl; +import io.swagger.v3.oas.models.media.Schema; +import org.testng.annotations.Test; + +import java.lang.reflect.Field; +import java.util.Iterator; +import java.util.Set; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +public class AnnotatedTypeCachingTest { + + @Test + public void testAnnotatedTypeEqualityIgnoresContextualFields() { + AnnotatedType type1 = new AnnotatedType(String.class) + .propertyName("userStatus"); + AnnotatedType type2 = new AnnotatedType(String.class) + .propertyName("city"); + assertEquals(type1, type2, "AnnotatedType objects with different contextual fields (e.g., propertyName) should be equal."); + assertEquals(type1.hashCode(), type2.hashCode(), "The hash codes of equal AnnotatedType objects must be the same."); + } + + static class User { + public String username; + public String email; + public Address address; + } + + static class Address { + public String street; + public String city; + } + + private static class DummyModelConverter implements ModelConverter { + @Override + public Schema resolve(AnnotatedType type, ModelConverterContext context, Iterator chain) { + if (type.getType().equals(User.class)) { + context.resolve(new AnnotatedType(String.class).propertyName("username")); + context.resolve(new AnnotatedType(String.class).propertyName("email")); + context.resolve(new AnnotatedType(Address.class).propertyName("address")); + return new Schema(); + } + if (type.getType().equals(Address.class)) { + context.resolve(new AnnotatedType(String.class).propertyName("street")); + context.resolve(new AnnotatedType(String.class).propertyName("city")); + return new Schema(); + } + return new Schema(); + } + } + + @Test + @SuppressWarnings("unchecked") + public void testCacheHitsForRepeatedStringTypeWithCorrectedEquals() throws Exception { + ModelConverterContextImpl context = new ModelConverterContextImpl(new DummyModelConverter()); + Schema userSchema = context.resolve(new AnnotatedType(User.class)); + assertNotNull(userSchema); + Field processedTypesField = ModelConverterContextImpl.class.getDeclaredField("processedTypes"); + processedTypesField.setAccessible(true); + Set processedTypes = (Set) processedTypesField.get(context); + long stringTypeCount = processedTypes.stream() + .filter(annotatedType -> annotatedType.getType().equals(String.class)) + .count(); + assertEquals(stringTypeCount, 1, "With the correct equals/hashCode, String type should be added to the cache only once."); + } +} \ No newline at end of file diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/converting/AnnotatedTypeTest.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/converting/AnnotatedTypeTest.java new file mode 100644 index 0000000000..855890273c --- /dev/null +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/converting/AnnotatedTypeTest.java @@ -0,0 +1,105 @@ +package io.swagger.v3.core.converting; + +import io.swagger.v3.core.converter.AnnotatedType; +import org.testng.annotations.Test; + +import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Type; +import java.util.HashSet; +import java.util.Set; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotEquals; +import static org.testng.Assert.assertTrue; + +public class AnnotatedTypeTest { + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @interface TestAnnA {} + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @interface TestAnnB {} + + @TestAnnA + @TestAnnB + @Deprecated + private static class AnnotationHolder {} + + private Annotation getAnnotationInstance(Class clazz) { + return AnnotationHolder.class.getAnnotation(clazz); + } + + /** + * Tests that equals() and hashCode() are order-insensitive for context annotations. + */ + @Test + public void testEqualsAndHashCode_shouldBeOrderInsensitiveForAnnotations() { + Annotation annA = getAnnotationInstance(TestAnnA.class); + Annotation annB = getAnnotationInstance(TestAnnB.class); + AnnotatedType type1 = new AnnotatedType(String.class).ctxAnnotations(new Annotation[]{annA, annB}); + AnnotatedType type2 = new AnnotatedType(String.class).ctxAnnotations(new Annotation[]{annB, annA}); + assertEquals(type1, type2, "Objects should be equal even if annotation order is different."); + assertEquals(type1.hashCode(), type2.hashCode(), "Hash codes should be equal even if annotation order is different."); + } + + /** + * Tests that JDK/internal annotations are filtered out for equals() and hashCode() comparison. + */ + @Test + public void testEqualsAndHashCode_shouldIgnoreJdkInternalAnnotations() { + Annotation annA = getAnnotationInstance(TestAnnA.class); + Annotation deprecated = getAnnotationInstance(Deprecated.class); + AnnotatedType typeWithUserAnn = new AnnotatedType(String.class).ctxAnnotations(new Annotation[]{annA}); + AnnotatedType typeWithJdkAnn = new AnnotatedType(String.class).ctxAnnotations(new Annotation[]{annA, deprecated}); + AnnotatedType typeWithOnlyJdkAnn = new AnnotatedType(String.class).ctxAnnotations(new Annotation[]{deprecated}); + AnnotatedType typeWithNoAnn = new AnnotatedType(String.class); + assertEquals(typeWithUserAnn, typeWithJdkAnn, "JDK annotations should be ignored in equality comparison."); + assertEquals(typeWithUserAnn.hashCode(), typeWithJdkAnn.hashCode(), "JDK annotations should be ignored in hashCode calculation."); + assertEquals(typeWithOnlyJdkAnn, typeWithNoAnn, "An object with only JDK annotations should be equal to one with no annotations."); + assertEquals(typeWithOnlyJdkAnn.hashCode(), typeWithNoAnn.hashCode(), "The hash code of an object with only JDK annotations should be the same as one with no annotations."); + } + + /** + * Tests that defensive copying prevents Set corruption from external array mutation. + */ + @Test + public void testImmutability_shouldPreventCorruptionInHashSet() { + Annotation annA = getAnnotationInstance(TestAnnA.class); + Annotation annB = getAnnotationInstance(TestAnnB.class); + Annotation[] originalAnnotations = new Annotation[]{annA}; + AnnotatedType type = new AnnotatedType(String.class).ctxAnnotations(originalAnnotations); + Set typeSet = new HashSet<>(); + typeSet.add(type); + int initialHashCode = type.hashCode(); + originalAnnotations[0] = annB; + assertEquals(initialHashCode, type.hashCode(), "Hash code must remain the same after mutating the external array."); + assertTrue(typeSet.contains(type), "The Set must still contain the object after mutating the external array."); + } + + /** + * Tests that an instance of a subclass can be equal to an instance of the parent class. + */ + @Test + public void testEqualsAndHashCode_shouldAllowSubclassEquality() { + class SubAnnotatedType extends AnnotatedType { + public SubAnnotatedType(Type type) { super(type); } + } + Annotation annA = getAnnotationInstance(TestAnnA.class); + Annotation[] annotations = {annA}; + AnnotatedType parent = new AnnotatedType(Integer.class).ctxAnnotations(annotations).name("number"); + SubAnnotatedType child = new SubAnnotatedType(Integer.class); + child.ctxAnnotations(annotations); + child.name("number"); + AnnotatedType differentParent = new AnnotatedType(Long.class).name("number"); + assertEquals(parent, child, "Parent and child objects should be equal if their properties are the same."); + assertEquals(child, parent, "Equality comparison should be symmetric."); + assertEquals(parent.hashCode(), child.hashCode(), "Parent and child hash codes should be equal if their properties are the same."); + assertNotEquals(parent, differentParent, "Objects with different properties should not be equal."); + } +} \ No newline at end of file diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/converting/ModelPropertyTest.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/converting/ModelPropertyTest.java index 4a749918ef..f55cbcf36d 100644 --- a/modules/swagger-core/src/test/java/io/swagger/v3/core/converting/ModelPropertyTest.java +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/converting/ModelPropertyTest.java @@ -103,6 +103,9 @@ public void testReadOnlyProperty() { public void testRequiredProperty() { final Map models = ModelConverters.getInstance().readAll(RequiredFields.class); Schema model = models.get("RequiredFields"); + assertFalse(model.getRequired().contains("optionalField")); + assertFalse(model.getRequired().contains("primitiveTypeWithoutConstraint")); + assertTrue(model.getRequired().contains("primitiveTypeWithConstraint")); assertTrue(model.getRequired().contains("required")); assertFalse(model.getRequired().contains("notRequired")); assertTrue(model.getRequired().contains("notRequiredWithAnnotation")); diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/filter/SpecFilterTest.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/filter/SpecFilterTest.java index 9ec9e786d1..7d161876d4 100644 --- a/modules/swagger-core/src/test/java/io/swagger/v3/core/filter/SpecFilterTest.java +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/filter/SpecFilterTest.java @@ -27,6 +27,8 @@ import io.swagger.v3.oas.models.parameters.Parameter; import io.swagger.v3.oas.models.tags.Tag; import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.testng.annotations.Test; import java.io.IOException; @@ -44,6 +46,7 @@ import static org.testng.Assert.fail; public class SpecFilterTest { + private static final Logger LOGGER = LoggerFactory.getLogger(SpecFilterTest.class); private static final String RESOURCE_RECURSIVE_MODELS = "specFiles/recursivemodels.json"; private static final String RESOURCE_PATH = "specFiles/petstore-3.0-v2.json"; @@ -189,7 +192,7 @@ public void run() { try { filteredMap.put("filtered " + id, new SpecFilter().filter(openAPI, new NoOpOperationsFilter(), null, null, null)); } catch (Exception e) { - e.printStackTrace(); + LOGGER.error("Failed to filter OpenAPI concurrently", e); } } }.start(); @@ -220,7 +223,7 @@ public void run() { } } } catch (Exception e) { - e.printStackTrace(); + LOGGER.error("Interrupted while waiting for filtering threads to complete", e); } for (OpenAPI filtered : filteredMap.values()) { assertEquals(Json.pretty(openAPI), Json.pretty(filtered)); diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/oas/models/RequiredFields.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/oas/models/RequiredFields.java index 9a1cf9f8b5..1257fd74be 100644 --- a/modules/swagger-core/src/test/java/io/swagger/v3/core/oas/models/RequiredFields.java +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/oas/models/RequiredFields.java @@ -3,6 +3,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import javax.validation.constraints.NotNull; +import java.util.Optional; public class RequiredFields { @Schema(description = "required", required = true) @@ -11,10 +12,20 @@ public class RequiredFields { @Schema(description = "not required") public Long notRequired; + @Schema(description = "Optional field") + public Optional optionalField; + @Schema(description = "not required with annotation") @NotNull public Long notRequiredWithAnnotation; + @Schema(description = "primitive type without constraint") + public long primitiveTypeWithoutConstraint; + + @Schema(description = "primitive type with constraint") + @NotNull + public long primitiveTypeWithConstraint; + @Schema(description = "mode auto", requiredMode = Schema.RequiredMode.AUTO) public Long modeAuto; diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/CompositionSuperfluousRefTest.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/CompositionSuperfluousRefTest.java new file mode 100644 index 0000000000..4682db0356 --- /dev/null +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/CompositionSuperfluousRefTest.java @@ -0,0 +1,143 @@ +package io.swagger.v3.core.resolving; + +import io.swagger.v3.core.converter.AnnotatedType; +import io.swagger.v3.core.converter.ModelConverters; +import io.swagger.v3.core.converter.ResolvedSchema; +import io.swagger.v3.oas.models.media.Schema; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class CompositionSuperfluousRefTest { + + static class SomeDto {} + static class OtherDto {} + + static class MyDtoOneOf { + @io.swagger.v3.oas.annotations.media.Schema(oneOf = { SomeDto.class, OtherDto.class }) + public Object myProperty; + } + + static class MyDtoWithAnyOf { + @io.swagger.v3.oas.annotations.media.Schema(anyOf = { SomeDto.class, OtherDto.class }) + public Object myProperty; + } + + static class MyDtoWithAllOf { + @io.swagger.v3.oas.annotations.media.Schema(allOf = { SomeDto.class, OtherDto.class }) + public Object myProperty; + } + + static class MyDtoWithoutComposition { + @io.swagger.v3.oas.annotations.media.Schema(implementation = SomeDto.class) + public Object myProperty; + } + + static class MyDtoWithNonMatchingRef { + @io.swagger.v3.oas.annotations.media.Schema( + ref = "#/components/schemas/ThirdDto", + oneOf = { SomeDto.class, OtherDto.class } + ) + public Object myProperty; + } + + @Test + public void oneOf_shouldNotHaveRef() { + ResolvedSchema rs = ModelConverters.getInstance(false) + .resolveAsResolvedSchema(new AnnotatedType(MyDtoOneOf.class)); + + Schema prop = (Schema) rs.schema.getProperties().get("myProperty"); + + Assert.assertNull(prop.get$ref()); + Assert.assertNotNull(prop.getOneOf()); + Assert.assertEquals(prop.getOneOf().size(), 2); + } + + @Test + public void anyOf_shouldNotHaveRef() { + ResolvedSchema rs = ModelConverters.getInstance(false) + .resolveAsResolvedSchema(new AnnotatedType(MyDtoWithAnyOf.class)); + + Schema prop = (Schema) rs.schema.getProperties().get("myProperty"); + + Assert.assertNull(prop.get$ref()); + Assert.assertNotNull(prop.getAnyOf()); + Assert.assertEquals(prop.getAnyOf().size(), 2); + } + + @Test + public void allOf_shouldNotHaveRef() { + + ResolvedSchema rs = ModelConverters.getInstance(false) + .resolveAsResolvedSchema(new AnnotatedType(MyDtoWithAllOf.class)); + + Schema prop = (Schema) rs.schema.getProperties().get("myProperty"); + Assert.assertNull(prop.get$ref()); + Assert.assertNotNull(prop.getAllOf()); + Assert.assertEquals(prop.getAllOf().size(), 2); + } + + @Test + public void testNonMatchingRef_shouldPreserveRef() { + ResolvedSchema rs = ModelConverters.getInstance(false) + .resolveAsResolvedSchema(new AnnotatedType(MyDtoWithNonMatchingRef.class)); + + Schema prop = (Schema) rs.schema.getProperties().get("myProperty"); + + Assert.assertNotNull(prop.get$ref()); + Assert.assertEquals(prop.get$ref(), "#/components/schemas/ThirdDto"); + //In 3.x refs cannot have siblings + Assert.assertNull(prop.getOneOf()); + } + + @Test + public void oneOf_shouldNotHaveRef31() { + ResolvedSchema rs = ModelConverters.getInstance(true) + .resolveAsResolvedSchema(new AnnotatedType(MyDtoOneOf.class)); + + Schema prop = (Schema) rs.schema.getProperties().get("myProperty"); + + Assert.assertNull(prop.get$ref()); + Assert.assertNotNull(prop.getOneOf()); + Assert.assertEquals(prop.getOneOf().size(), 2); + } + + @Test + public void anyOf_shouldNotHaveRef31() { + ResolvedSchema rs = ModelConverters.getInstance(true) + .resolveAsResolvedSchema(new AnnotatedType(MyDtoWithAnyOf.class)); + + Schema prop = (Schema) rs.schema.getProperties().get("myProperty"); + + Assert.assertNull(prop.get$ref()); + Assert.assertNotNull(prop.getAnyOf()); + Assert.assertEquals(prop.getAnyOf().size(), 2); + } + + @Test + public void allOf_shouldNotHaveRef31() { + + ResolvedSchema rs = ModelConverters.getInstance(true) + .resolveAsResolvedSchema(new AnnotatedType(MyDtoWithAllOf.class)); + + Schema prop = (Schema) rs.schema.getProperties().get("myProperty"); + Assert.assertNull(prop.get$ref()); + Assert.assertNotNull(prop.getAllOf()); + Assert.assertEquals(prop.getAllOf().size(), 2); + } + + @Test + public void testNonMatchingRef_shouldPreserveRef31() { + ResolvedSchema rs = ModelConverters.getInstance(true) + .resolveAsResolvedSchema(new AnnotatedType(MyDtoWithNonMatchingRef.class)); + + Schema prop = (Schema) rs.schema.getProperties().get("myProperty"); + + Assert.assertNotNull(prop.get$ref()); + Assert.assertEquals(prop.get$ref(), "#/components/schemas/ThirdDto"); + Assert.assertNotNull(prop.getOneOf()); + Assert.assertEquals(prop.getOneOf().size(), 2); + } + +} + + diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/EnumTest.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/EnumTest.java index 72dd08ee12..6f2f552353 100644 --- a/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/EnumTest.java +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/EnumTest.java @@ -9,13 +9,11 @@ import io.swagger.v3.oas.models.media.StringSchema; import org.testng.annotations.Test; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; +import java.util.*; +import java.util.stream.Collectors; -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertNotNull; -import static org.testng.Assert.assertTrue; +import static org.testng.Assert.*; +import static org.testng.AssertJUnit.assertFalse; public class EnumTest extends SwaggerTestBase { @@ -33,7 +31,6 @@ public void testEnum() { new ArrayList(Collections2.transform(Arrays.asList(Currency.values()), Functions.toStringFunction())); assertEquals(strModel.getEnum(), modelValues); - final Schema property = context.resolve(new AnnotatedType().type(Currency.class).schemaProperty(true)); assertNotNull(property); assertTrue(property instanceof StringSchema); @@ -50,10 +47,262 @@ public void testEnumGenerics() { final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver); final Schema model = context.resolve((new AnnotatedType().type(Contract.class))); + assertBasicModelStructure(model, "Contract"); + assertPropertyExists(model, "type"); + } + + @Test + public void testEnumPropertyWithSchemaAnnotation() { + final ModelResolver modelResolver = new ModelResolver(mapper()).openapi31(true); + final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver); + + final Schema model = context.resolve(new AnnotatedType().type(ClassWithEnumAsRefProperty.class)); + assertBasicModelStructure(model, "ClassWithEnumAsRefProperty"); + assertPropertyExists(model, "enumWithSchemaProperty"); + + final Schema enumPropertySchema = (Schema) model.getProperties().get("enumWithSchemaProperty"); + assertEnumAsRefProperty(enumPropertySchema, "#/components/schemas/EnumWithSchemaProperty"); + assertEquals(enumPropertySchema.getDescription(), "Property description"); + } + + @Test + public void testEnumPropertyWithGlobalSwitchOnlyOpenApi31() { + ModelResolver.enumsAsRef = true; + final ModelResolver modelResolver = new ModelResolver(mapper()).openapi31(true); + final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver); + + final Schema model = context.resolve(new AnnotatedType().type(ClassWithPlainEnum.class)); + assertBasicModelStructure(model, "ClassWithPlainEnum"); + assertPropertyExists(model, "plainEnum"); + + final Schema enumPropertySchema = (Schema) model.getProperties().get("plainEnum"); + assertNotNull(enumPropertySchema.get$ref()); + assertNull(enumPropertySchema.getEnum()); + assertEquals(enumPropertySchema.getDescription(), "Plain enum property"); + + assertEnumComponentExists(context, ClassWithPlainEnum.PlainEnum.values(), null); + + // Reset the static field + ModelResolver.enumsAsRef = false; + } + + @Test + public void testArrayOfEnumWithSchemaAnnotationOpenApi31() { + final ModelResolver modelResolver = new ModelResolver(mapper()).openapi31(true); + final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver); + + final Schema model = context.resolve(new AnnotatedType().type(ClassWithEnumArray.class)); + assertBasicModelStructure(model, "ClassWithEnumArray"); + assertPropertyExists(model, "enumArray"); + + final Schema arrayPropertySchema = (Schema) model.getProperties().get("enumArray"); + assertArrayWithEnumRef(arrayPropertySchema); + + assertEnumComponentExists(context, ClassWithEnumArray.ArrayEnum.values(), "Enum description"); + } + + @Test + public void testArrayOfEnumWithSchemaAnnotationOpenApi30() { + final ModelResolver modelResolver = new ModelResolver(mapper()).openapi31(false); + final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver); + + final Schema model = context.resolve(new AnnotatedType().type(ClassWithEnumArray.class)); + assertBasicModelStructure(model, "ClassWithEnumArray"); + assertPropertyExists(model, "enumArray"); + + final Schema arrayPropertySchema = (Schema) model.getProperties().get("enumArray"); + assertArrayWithEnumRef(arrayPropertySchema); + + assertEnumComponentExists(context, ClassWithEnumArray.ArrayEnum.values(), "Enum description"); + } + + @Test + public void testControlTestNoRefOpenApi31() { + ModelResolver.enumsAsRef = false; + final ModelResolver modelResolver = new ModelResolver(mapper()).openapi31(true); + final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver); + + final Schema model = context.resolve(new AnnotatedType().type(ClassWithPlainEnum.class)); + assertBasicModelStructure(model, "ClassWithPlainEnum"); + assertPropertyExists(model, "plainEnum"); + + final Schema enumPropertySchema = (Schema) model.getProperties().get("plainEnum"); + assertInlineEnumProperty(enumPropertySchema); + + // Apply broad assertions - verify no components are created for inline enums + Map components = context.getDefinedModels(); + if (components != null && !components.isEmpty()) { + Set expected = Arrays.stream(ClassWithPlainEnum.PlainEnum.values()) + .map(Enum::name) + .collect(Collectors.toSet()); + Schema enumComponent = findEnumComponent(components, expected); + assertNull(enumComponent, "No enum component should exist for inline enums"); + } + } + + @Test + public void testControlTestNoRefOpenApi30() { + ModelResolver.enumsAsRef = false; + final ModelResolver modelResolver = new ModelResolver(mapper()).openapi31(false); + final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver); + + final Schema model = context.resolve(new AnnotatedType().type(ClassWithPlainEnum.class)); + assertBasicModelStructure(model, "ClassWithPlainEnum"); + assertPropertyExists(model, "plainEnum"); + + final Schema enumPropertySchema = (Schema) model.getProperties().get("plainEnum"); + assertInlineEnumProperty(enumPropertySchema); + + // Apply broad assertions - verify no components are created for inline enums + Map components = context.getDefinedModels(); + if (components != null && !components.isEmpty()) { + Set expected = Arrays.stream(ClassWithPlainEnum.PlainEnum.values()) + .map(Enum::name) + .collect(Collectors.toSet()); + Schema enumComponent = findEnumComponent(components, expected); + assertNull(enumComponent, "No enum component should exist for inline enums"); + } + } + + @Test + public void testEnumWithAllOfSchemaResolutionOpenApi30() { + final ModelResolver modelResolver = new ModelResolver(mapper()) + .openapi31(false) + .schemaResolution(Schema.SchemaResolution.ALL_OF); + final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver); + + final Schema model = context.resolve(new AnnotatedType().type(ClassWithEnumAsRefProperty.class)); + assertBasicModelStructure(model, "ClassWithEnumAsRefProperty"); + assertPropertyExists(model, "enumWithSchemaProperty"); + + final Schema enumPropertySchema = (Schema) model.getProperties().get("enumWithSchemaProperty"); + + boolean hasEnumRef = false; + for (Object allOfItem : enumPropertySchema.getAllOf()) { + if (allOfItem instanceof Schema) { + Schema allOfSchema = (Schema) allOfItem; + if ("#/components/schemas/EnumWithSchemaProperty".equals(allOfSchema.get$ref())) { + hasEnumRef = true; + break; + } + } + } + assertTrue(hasEnumRef, "AllOf should contain reference to enum component"); + assertEnumComponentExists(context, ClassWithEnumAsRefProperty.EnumWithSchemaProperty.values(), "Enum description"); + } + + private void assertBasicModelStructure(Schema model, String expectedName) { assertNotNull(model); - assertEquals(model.getName(), "Contract"); - assertTrue(model.getProperties().containsKey("type")); - assertNotNull(model.getProperties().get("type")); + assertEquals(model.getName(), expectedName); + } + + private void assertPropertyExists(Schema model, String propertyName) { + assertTrue(model.getProperties().containsKey(propertyName)); + assertNotNull(model.getProperties().get(propertyName)); + } + + private void assertEnumComponentExists(ModelConverterContextImpl context, Enum[] enumValues, String expectedDescription) { + Map components = context.getDefinedModels(); + assertNotNull(components); + assertFalse(components.isEmpty()); + + Set expected = Arrays.stream(enumValues) + .map(Enum::name) + .collect(Collectors.toCollection(LinkedHashSet::new)); + + Schema enumComponent = findEnumComponent(components, expected); + assertNotNull(enumComponent); + assertEquals(enumComponent.getDescription(), expectedDescription); + } + + private void assertEnumComponentExistsWithDefault(ModelConverterContextImpl context, Enum[] enumValues, String expectedDescription, String expectedDefault) { + assertEnumComponentExists(context, enumValues, expectedDescription); + + Map components = context.getDefinedModels(); + Set expected = Arrays.stream(enumValues) + .map(Enum::name) + .collect(Collectors.toCollection(LinkedHashSet::new)); + + Schema enumComponent = findEnumComponent(components, expected); + assertEquals(enumComponent.getDefault(), expectedDefault); + } + + private Schema findEnumComponent(Map components, Set expectedValues) { + return components.values().stream() + .filter(Objects::nonNull) + .filter(s -> s.getEnum() != null) + .filter(s -> { + List ev = s.getEnum(); + Set vals = ev.stream().map(Object::toString).collect(Collectors.toSet()); + return vals.containsAll(expectedValues) && expectedValues.containsAll(vals); + }) + .findFirst() + .orElse(null); + } + + private void assertEnumAsRefProperty(Schema propertySchema, String expectedRef) { + assertEquals(propertySchema.get$ref(), expectedRef); + assertNull(propertySchema.getEnum()); + } + + private void assertInlineEnumProperty(Schema propertySchema) { + assertNotNull(propertySchema.getEnum()); + assertNull(propertySchema.get$ref()); + } + + private void assertArrayWithEnumRef(Schema arrayPropertySchema) { + assertNotNull(arrayPropertySchema.getItems()); + assertNotNull(arrayPropertySchema.getItems().get$ref()); + } + + + public static class ClassWithEnumAsRefProperty { + + @io.swagger.v3.oas.annotations.media.Schema(enumAsRef = true, description = "Property description", maximum = "1923234") + public final EnumWithSchemaProperty enumWithSchemaProperty; + + public ClassWithEnumAsRefProperty(EnumWithSchemaProperty enumWithSchemaProperty) { + this.enumWithSchemaProperty = enumWithSchemaProperty; + } + + @io.swagger.v3.oas.annotations.media.Schema(description = "Enum description") + public enum EnumWithSchemaProperty { + VALUE1, + VALUE2 + } + } + + public static class ClassWithEnumArray { + + @io.swagger.v3.oas.annotations.media.ArraySchema(schema = @io.swagger.v3.oas.annotations.media.Schema(enumAsRef = true, description = "Property Description")) + public final ArrayEnum[] enumArray; + + public ClassWithEnumArray(ArrayEnum[] enumArray) { + this.enumArray = enumArray; + } + + @io.swagger.v3.oas.annotations.media.Schema(description = "Enum description") + public enum ArrayEnum { + FIRST, + SECOND, + THIRD + } + } + + public static class ClassWithPlainEnum { + + @io.swagger.v3.oas.annotations.media.Schema(description = "Plain enum property") + public final PlainEnum plainEnum; + + public ClassWithPlainEnum(PlainEnum plainEnum) { + this.plainEnum = plainEnum; + } + + public enum PlainEnum { + ONE, + TWO, + THREE + } } public enum Currency { diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/SwaggerTestBase.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/SwaggerTestBase.java index a27d724562..883ce03423 100644 --- a/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/SwaggerTestBase.java +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/SwaggerTestBase.java @@ -6,9 +6,12 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import io.swagger.v3.core.jackson.ModelResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public abstract class SwaggerTestBase { static ObjectMapper mapper; + private static final Logger LOGGER = LoggerFactory.getLogger(SwaggerTestBase.class); public static ObjectMapper mapper() { if (mapper == null) { @@ -26,9 +29,9 @@ protected ModelResolver modelResolver() { protected void prettyPrint(Object o) { try { - System.out.println(mapper().writer(new DefaultPrettyPrinter()).writeValueAsString(o)); + LOGGER.debug(mapper().writer(new DefaultPrettyPrinter()).writeValueAsString(o)); } catch (Exception e) { - e.printStackTrace(); + LOGGER.error("Failed to pretty print object", e); } } } diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/Ticket4800Test.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/Ticket4800Test.java new file mode 100644 index 0000000000..a2475a1318 --- /dev/null +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/Ticket4800Test.java @@ -0,0 +1,67 @@ +package io.swagger.v3.core.resolving; + +import io.swagger.v3.core.converter.ModelConverters; +import io.swagger.v3.core.matchers.SerializationMatchers; +import io.swagger.v3.oas.models.media.Schema; +import org.testng.annotations.Test; + +import java.util.Map; + +public class Ticket4800Test extends SwaggerTestBase{ + + @Test(description = "Custom schema implementation in property and enum as ref type value") + public void testCustomSchemaImplementation() { + + String expectedYaml = "ModelWithCustomSchemaImplementationInProperty:\n" + + " type: object\n" + + " properties:\n" + + " enumExampleFieldWithImplementationProp:\n" + + " $ref: \"#/components/schemas/MyEnum\"\n" + + " default: \"yes\"\n" + + " description: Prop description\n" + + " secondExampleFieldWithTypeProp:\n" + + " type: string\n" + + "MyEnum:\n" + + " type: string\n" + + " enum:\n" + + " - \"yes\"\n" + + " - \"no\""; + + Map stringSchemaMap = ModelConverters.getInstance(true).readAll(Ticket4800Test.ModelWithCustomSchemaImplementationInProperty.class); + SerializationMatchers.assertEqualsToYaml31(stringSchemaMap, expectedYaml); + } + + static class ModelWithCustomSchemaImplementationInProperty { + + @io.swagger.v3.oas.annotations.media.Schema(implementation = MyEnum.class, description = "Prop description", defaultValue = "yes", enumAsRef = true) + private MyEnum enumExampleFieldWithImplementationProp; + + @io.swagger.v3.oas.annotations.media.Schema(type = "string", enumAsRef = true) + private MyEnum2 secondExampleFieldWithTypeProp; + + public MyEnum getEnumExampleFieldWithImplementationProp() { + return enumExampleFieldWithImplementationProp; + } + + public void setEnumExampleFieldWithImplementationProp(MyEnum enumExampleFieldWithImplementationProp) { + this.enumExampleFieldWithImplementationProp = enumExampleFieldWithImplementationProp; + } + + public MyEnum2 getSecondExampleFieldWithTypeProp() { + return secondExampleFieldWithTypeProp; + } + + public void setSecondExampleFieldWithTypeProp(MyEnum2 secondExampleFieldWithTypeProp) { + this.secondExampleFieldWithTypeProp = secondExampleFieldWithTypeProp; + } + + enum MyEnum { + yes, no + } + + //Should not be included in the model definitions + enum MyEnum2 { + si, no + } + } +} diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/Ticket4904Test.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/Ticket4904Test.java new file mode 100644 index 0000000000..3a61aeeef3 --- /dev/null +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/Ticket4904Test.java @@ -0,0 +1,304 @@ +package io.swagger.v3.core.resolving; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.core.converter.AnnotatedType; +import io.swagger.v3.core.converter.ModelConverterContextImpl; +import io.swagger.v3.core.jackson.ModelResolver; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.models.media.ComposedSchema; +import io.swagger.v3.oas.models.media.JsonSchema; +import org.testng.annotations.Test; + +import static io.swagger.v3.core.resolving.SwaggerTestBase.mapper; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +public class Ticket4904Test { + + @Test + public void testComposedSchemaWithDiscriminator() { + final ModelResolver modelResolver = new ModelResolver(mapper()); + modelResolver.setOpenapi31(true); + final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver); + + io.swagger.v3.oas.models.media.Schema schema = context.resolve(new AnnotatedType(ParentClass.class)); + + assertNotNull(schema); + assertTrue(schema instanceof JsonSchema, "Expected JsonSchema in OpenAPI 3.1 mode, but got: " + schema.getClass()); + JsonSchema jsonSchema = (JsonSchema) schema; + assertNotNull(jsonSchema.getOneOf()); + assertEquals(jsonSchema.getOneOf().size(), 2); + assertEquals(( jsonSchema.getOneOf().get(0)).get$ref(), "#/components/schemas/ChildClassA"); + assertEquals(( jsonSchema.getOneOf().get(1)).get$ref(), "#/components/schemas/ChildClassB"); + assertNotNull(jsonSchema.getDiscriminator()); + assertEquals(jsonSchema.getDiscriminator().getPropertyName(), "objectType"); + assertNotNull(jsonSchema.getDiscriminator().getMapping()); + assertEquals(jsonSchema.getDiscriminator().getMapping().size(), 2); + assertEquals(jsonSchema.getDiscriminator().getMapping().get("A"), "#/components/schemas/ChildClassA"); + assertEquals(jsonSchema.getDiscriminator().getMapping().get("B"), "#/components/schemas/ChildClassB"); + } + + @Test + public void testMultiElementAllOf() { + final ModelResolver modelResolver = new ModelResolver(mapper()); + modelResolver.setOpenapi31(true); + final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver); + + io.swagger.v3.oas.models.media.Schema schema = context.resolve(new AnnotatedType(MultiAllOfClass.class)); + + assertNotNull(schema); + assertTrue(schema instanceof JsonSchema, "Expected JsonSchema in OpenAPI 3.1 mode, but got: " + schema.getClass()); + assertNotNull(schema.getAllOf()); + assertEquals(schema.getAllOf().size(), 3); + assertEquals(((io.swagger.v3.oas.models.media.Schema) schema.getAllOf().get(0)).get$ref(), "#/components/schemas/ClassA"); + assertEquals(((io.swagger.v3.oas.models.media.Schema) schema.getAllOf().get(1)).get$ref(), "#/components/schemas/ClassB"); + assertEquals(((io.swagger.v3.oas.models.media.Schema) schema.getAllOf().get(2)).get$ref(), "#/components/schemas/ClassC"); + } + + @Test + public void testMultiElementAllOfOpenApi30() { + final ModelResolver modelResolver = new ModelResolver(mapper()); + modelResolver.setOpenapi31(false); + final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver); + + io.swagger.v3.oas.models.media.Schema schema = context.resolve(new AnnotatedType(MultiAllOfClass.class)); + + assertNotNull(schema); + assertTrue(schema instanceof ComposedSchema, "Expected ComposedSchema in OpenAPI 3.0 mode, but got: " + schema.getClass()); + ComposedSchema composedSchema = (ComposedSchema) schema; + assertNotNull(composedSchema.getAllOf()); + assertEquals(composedSchema.getAllOf().size(), 3); + assertEquals(composedSchema.getAllOf().get(0).get$ref(), "#/components/schemas/ClassA"); + assertEquals(composedSchema.getAllOf().get(1).get$ref(), "#/components/schemas/ClassB"); + assertEquals(composedSchema.getAllOf().get(2).get$ref(), "#/components/schemas/ClassC"); + } + + @Test + public void testMixedComposition() { + final ModelResolver modelResolver = new ModelResolver(mapper()); + modelResolver.setOpenapi31(true); + final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver); + + io.swagger.v3.oas.models.media.Schema schema = context.resolve(new AnnotatedType(MixedCompositionClass.class)); + + assertNotNull(schema); + assertTrue(schema instanceof JsonSchema, "Expected JsonSchema in OpenAPI 3.1 mode, but got: " + schema.getClass()); + + JsonSchema jsonSchema = (JsonSchema) schema; + assertNotNull(jsonSchema.getAllOf()); + assertEquals(jsonSchema.getAllOf().size(), 3); + assertEquals(jsonSchema.getAllOf().get(0).get$ref(), "#/components/schemas/ClassA"); + assertEquals(jsonSchema.getAllOf().get(1).get$ref(), "#/components/schemas/ClassB"); + io.swagger.v3.oas.models.media.Schema inlineSchema = jsonSchema.getAllOf().get(2); + assertNotNull(inlineSchema); + assertEquals(inlineSchema.get$ref(), null); + assertNotNull(inlineSchema.getProperties()); + assertTrue(inlineSchema.getProperties().containsKey("additionalProperty")); + io.swagger.v3.oas.models.media.Schema additionalPropertySchema = + (io.swagger.v3.oas.models.media.Schema) inlineSchema.getProperties().get("additionalProperty"); + assertNotNull(additionalPropertySchema); + assertEquals(additionalPropertySchema.getDescription(), "Additional property"); + } + + @Test + public void testAnyOfComposition() { + final ModelResolver modelResolver = new ModelResolver(mapper()); + modelResolver.setOpenapi31(true); + final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver); + + io.swagger.v3.oas.models.media.Schema schema = context.resolve(new AnnotatedType(AnyOfClass.class)); + + assertNotNull(schema); + assertTrue(schema instanceof JsonSchema, "Expected JsonSchema in OpenAPI 3.1 mode, but got: " + schema.getClass()); + JsonSchema jsonSchema = (JsonSchema) schema; + assertNotNull(jsonSchema.getAnyOf()); + assertEquals(jsonSchema.getAnyOf().size(), 3); + assertEquals(((io.swagger.v3.oas.models.media.Schema) jsonSchema.getAnyOf().get(0)).get$ref(), "#/components/schemas/ClassA"); + assertEquals(((io.swagger.v3.oas.models.media.Schema) jsonSchema.getAnyOf().get(1)).get$ref(), "#/components/schemas/ClassB"); + assertEquals(((io.swagger.v3.oas.models.media.Schema) jsonSchema.getAnyOf().get(2)).get$ref(), "#/components/schemas/ClassC"); + } + + @Test + public void testOneOfWithReferences() { + final ModelResolver modelResolver = new ModelResolver(mapper()); + modelResolver.setOpenapi31(true); + final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver); + + io.swagger.v3.oas.models.media.Schema schema = context.resolve(new AnnotatedType(OneOfWithRefsClass.class)); + + assertNotNull(schema); + assertTrue(schema instanceof JsonSchema, "Expected JsonSchema in OpenAPI 3.1 mode, but got: " + schema.getClass()); + JsonSchema jsonSchema = (JsonSchema) schema; + assertNotNull(jsonSchema.getOneOf()); + assertEquals(jsonSchema.getOneOf().size(), 2); + assertEquals((jsonSchema.getOneOf().get(0)).get$ref(), "#/components/schemas/RefSubtypeA"); + assertEquals((jsonSchema.getOneOf().get(1)).get$ref(), "#/components/schemas/RefSubtypeB"); + } + + @Test + public void testJacksonPolymorphismWithTypeInfo() { + final ModelResolver modelResolver = new ModelResolver(mapper()); + modelResolver.setOpenapi31(true); + final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver); + + io.swagger.v3.oas.models.media.Schema schema = context.resolve(new AnnotatedType(JacksonPolymorphicParent.class)); + + assertNotNull(schema); + assertTrue(schema instanceof JsonSchema, "Expected JsonSchema in OpenAPI 3.1 mode, but got: " + schema.getClass()); + JsonSchema jsonSchema = (JsonSchema) schema; + if (jsonSchema.getOneOf() != null) { + assertEquals(jsonSchema.getOneOf().size(), 2); + assertEquals((jsonSchema.getOneOf().get(0)).get$ref(), "#/components/schemas/JacksonChildA"); + assertEquals((jsonSchema.getOneOf().get(1)).get$ref(), "#/components/schemas/JacksonChildB"); + assertNotNull(jsonSchema.getDiscriminator()); + assertEquals(jsonSchema.getDiscriminator().getPropertyName(), "type"); + } else { + assertNotNull(jsonSchema.getProperties()); + assertTrue(jsonSchema.getProperties().containsKey("commonProperty") || + jsonSchema.getProperties().containsKey("type"), + "Expected to find either commonProperty or type discriminator property"); + } + } + + @Test + public void testComplexAllOfWithInlineProperties() { + final ModelResolver modelResolver = new ModelResolver(mapper()); + modelResolver.setOpenapi31(true); + final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver); + + io.swagger.v3.oas.models.media.Schema schema = context.resolve(new AnnotatedType(ComplexAllOfClass.class)); + + assertNotNull(schema); + assertTrue(schema instanceof JsonSchema, "Expected JsonSchema in OpenAPI 3.1 mode, but got: " + schema.getClass()); + JsonSchema jsonSchema = (JsonSchema) schema; + assertNotNull(jsonSchema.getAllOf()); + assertEquals(jsonSchema.getAllOf().size(), 3); + assertEquals((jsonSchema.getAllOf().get(0)).get$ref(), "#/components/schemas/ClassA"); + assertEquals((jsonSchema.getAllOf().get(1)).get$ref(), "#/components/schemas/ClassB"); + io.swagger.v3.oas.models.media.Schema inlineSchema = jsonSchema.getAllOf().get(2); + assertNotNull(inlineSchema); + assertEquals(inlineSchema.get$ref(), null); + assertNotNull(inlineSchema.getProperties()); + assertTrue(inlineSchema.getProperties().containsKey("complexProperty")); + assertTrue(inlineSchema.getProperties().containsKey("additionalField")); + } + + @Test + public void testNestedComposition() { + final ModelResolver modelResolver = new ModelResolver(mapper()); + modelResolver.setOpenapi31(true); + final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver); + + io.swagger.v3.oas.models.media.Schema schema = context.resolve(new AnnotatedType(NestedCompositionClass.class)); + + assertNotNull(schema); + assertTrue(schema instanceof JsonSchema, "Expected JsonSchema in OpenAPI 3.1 mode, but got: " + schema.getClass()); + JsonSchema jsonSchema = (JsonSchema) schema; + assertNotNull(jsonSchema.getOneOf()); + assertNotNull(jsonSchema.getAllOf()); + assertEquals(jsonSchema.getOneOf().size(), 2); + assertEquals(jsonSchema.getAllOf().size(), 1); + assertEquals((jsonSchema.getOneOf().get(0)).get$ref(), "#/components/schemas/ChildClassA"); + assertEquals((jsonSchema.getOneOf().get(1)).get$ref(), "#/components/schemas/ChildClassB"); + assertEquals((jsonSchema.getAllOf().get(0)).get$ref(), "#/components/schemas/ClassA"); + } + + @Schema( + type = "object", + discriminatorMapping = { + @DiscriminatorMapping(value = "A", schema = ChildClassA.class), + @DiscriminatorMapping(value = "B", schema = ChildClassB.class) + }, + oneOf = {ChildClassA.class, ChildClassB.class}, + discriminatorProperty = "objectType" + ) + public abstract static class ParentClass { + } + + public static class ChildClassA extends ParentClass { + } + + public static class ChildClassB extends ParentClass { + } + + @Schema(allOf = {ClassA.class, ClassB.class, ClassC.class}) + public static class MultiAllOfClass { + } + + @Schema( + allOf = {ClassA.class, ClassB.class}, + type = "object", + description = "Mixed composition with allOf and additional properties", + format = "custom-format" + ) + public static class MixedCompositionClass { + @Schema(description = "Additional property") + public String additionalProperty; + } + + public static class ClassA { + public String propertyA; + } + + public static class ClassB { + public String propertyB; + } + + public static class ClassC { + public String propertyC; + } + + @Schema(anyOf = {ClassA.class, ClassB.class, ClassC.class}) + public static class AnyOfClass { + } + + @Schema(oneOf = {RefSubtypeA.class, RefSubtypeB.class}) + public static class OneOfWithRefsClass { + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") + @JsonSubTypes({ + @JsonSubTypes.Type(value = JacksonChildA.class, name = "A"), + @JsonSubTypes.Type(value = JacksonChildB.class, name = "B") + }) + public static class JacksonPolymorphicParent { + public String commonProperty; + } + + public static class JacksonChildA extends JacksonPolymorphicParent { + public String childAProperty; + } + + public static class JacksonChildB extends JacksonPolymorphicParent { + public String childBProperty; + } + + @Schema(allOf = {ClassA.class, ClassB.class}) + public static class ComplexAllOfClass { + @Schema(description = "Complex property with nested structure") + public String complexProperty; + + @Schema(description = "Additional field for testing") + public String additionalField; + } + + @Schema( + oneOf = {ChildClassA.class, ChildClassB.class}, + allOf = {ClassA.class} + ) + public static class NestedCompositionClass { + } + + // Additional classes for $ref subtypes testing + public static class RefSubtypeA { + public String refPropertyA; + } + + public static class RefSubtypeB { + public String refPropertyB; + } + +} diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/v31/ModelResolverOAS31Test.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/v31/ModelResolverOAS31Test.java index 542cb87007..13b3d16213 100644 --- a/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/v31/ModelResolverOAS31Test.java +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/v31/ModelResolverOAS31Test.java @@ -30,8 +30,10 @@ public void testAnnotatedArray() { SerializationMatchers.assertEqualsToYaml31(model, "type: array\n" + "contains:\n" + " type: string\n" + + "description: arraydescription\n" + "items:\n" + " type: string\n" + + " description: itemdescription\n"+ "maxContains: 10\n" + "minContains: 1\n" + "prefixItems:\n" + @@ -85,6 +87,26 @@ public void testOAS31Fields() { " properties:\n" + " billingAddress:\n" + " type: string\n" + + " acceptingCountries:\n" + + " type: array\n" + + " items:\n" + + " type: string\n" + + " description: accepting country\n" + + " enum:\n" + + " - UNITED_STATES_OF_AMERICA\n" + + " - CANADA\n" + + " uniqueItems: true\n" + + " availableCurrencies:\n" + + " type: array\n" + + " items:\n" + + " $ref: \"#/components/schemas/Currency\"\n" + + " description: available currencies\n" + + " uniqueItems: true\n" + + "Currency:\n" + + " type: object\n" + + " properties:\n" + + " currencyCode:\n" + + " type: string\n" + "ModelWithOAS31Stuff:\n" + " type: object\n" + " $comment: Random comment at schema level\n" + @@ -95,12 +117,16 @@ public void testOAS31Fields() { " type: array\n" + " contains:\n" + " type: string\n" + + " description: arraydescription\n" + " items:\n" + " type: string\n" + + " description: itemdescription\n" + + " title: itemtitle\n" + " maxContains: 10\n" + " minContains: 1\n" + " prefixItems:\n" + " - type: string\n" + + " title: arraytitle\n" + " unevaluatedItems:\n" + " type: number\n" + " status:\n" + diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/v31/model/AnnotatedArray.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/v31/model/AnnotatedArray.java index 9e49f750d6..cc5e4c84f7 100644 --- a/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/v31/model/AnnotatedArray.java +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/v31/model/AnnotatedArray.java @@ -13,8 +13,10 @@ types = { "number" } ), schema = @Schema( - types = { "string" } + types = { "string" }, + description = "itemdescription" ), + arraySchema = @Schema(description = "arraydescription"), prefixItems = { @Schema( types = { "string" } diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/v31/model/CreditCard.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/v31/model/CreditCard.java index fc8d4c900c..7a66fb13fe 100644 --- a/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/v31/model/CreditCard.java +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/v31/model/CreditCard.java @@ -1,8 +1,15 @@ package io.swagger.v3.core.resolving.v31.model; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.Set; + public class CreditCard { private String billingAddress; + private Set acceptingCountries; + private Set availableCurrencies; public String getBillingAddress() { return billingAddress; @@ -11,4 +18,22 @@ public String getBillingAddress() { public void setBillingAddress(String billingAddress) { this.billingAddress = billingAddress; } + + @ArraySchema(schema = @Schema(description = "accepting country")) + public Set getAcceptingCountries() { + return acceptingCountries; + } + + public void setAcceptingCountries(Set acceptingCountries) { + this.acceptingCountries = acceptingCountries; + } + + @ArraySchema(schema = @Schema(description = "available currencies")) + public Set getAvailableCurrencies() { + return availableCurrencies; + } + + public void setAvailableCurrencies(Set availableCurrencies) { + this.availableCurrencies = availableCurrencies; + } } diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/v31/model/Currency.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/v31/model/Currency.java new file mode 100644 index 0000000000..b13e5adbf5 --- /dev/null +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/v31/model/Currency.java @@ -0,0 +1,14 @@ +package io.swagger.v3.core.resolving.v31.model; + +public class Currency { + + private String currencyCode; + + public String getCurrencyCode() { + return currencyCode; + } + + public void setCurrencyCode(String currencyCode) { + this.currencyCode = currencyCode; + } +} \ No newline at end of file diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/v31/model/ModelWithOAS31Stuff.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/v31/model/ModelWithOAS31Stuff.java index c1ef938175..ec3615dd3e 100644 --- a/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/v31/model/ModelWithOAS31Stuff.java +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/v31/model/ModelWithOAS31Stuff.java @@ -23,6 +23,13 @@ public class ModelWithOAS31Stuff { private Client client; @ArraySchema( + schema = @Schema( + types = { "string" }, + description = "itemdescription", + title = "itemtitle" + + ), + arraySchema = @Schema(description = "arraydescription", title = "arraytitle"), maxContains = 10, minContains = 1, contains = @Schema( diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/serialization/OpenAPI3_1SerializationTest.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/serialization/OpenAPI3_1SerializationTest.java index 3f1d1a8f85..53e338d0a8 100644 --- a/modules/swagger-core/src/test/java/io/swagger/v3/core/serialization/OpenAPI3_1SerializationTest.java +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/serialization/OpenAPI3_1SerializationTest.java @@ -29,6 +29,8 @@ import io.swagger.v3.oas.models.responses.ApiResponse; import io.swagger.v3.oas.models.responses.ApiResponses; import io.swagger.v3.oas.models.security.SecurityScheme; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.testng.annotations.Test; import org.yaml.snakeyaml.LoaderOptions; @@ -37,6 +39,7 @@ import static org.testng.Assert.assertTrue; public class OpenAPI3_1SerializationTest { + private static final Logger LOGGER = LoggerFactory.getLogger(OpenAPI3_1SerializationTest.class); @Test public void testSerializePetstore() throws Exception { @@ -1390,7 +1393,7 @@ public void testBooleanSchemaSerialization() { .openapi("3.1.0") .components(new Components().addSchemas("test", new Schema().booleanSchemaValue(true))); - System.out.println("--------- root ----------"); + LOGGER.debug("--------- root ----------"); Json31.prettyPrint(openAPI); assertEquals(Json31.pretty(openAPI), withJacksonSystemLineSeparator("{\n" + " \"openapi\" : \"3.1.0\",\n" + @@ -1400,19 +1403,19 @@ public void testBooleanSchemaSerialization() { " }\n" + " }\n" + "}")); - System.out.println("--------- schema ----------"); + LOGGER.debug("--------- schema ----------"); Json31.prettyPrint(openAPI.getComponents().getSchemas().get("test")); assertEquals(Json31.pretty(openAPI.getComponents().getSchemas().get("test")), "true"); - System.out.println("--------- root YAML----------"); + LOGGER.debug("--------- root YAML----------"); Yaml31.prettyPrint(openAPI); assertEquals(Yaml31.pretty(openAPI), "openapi: 3.1.0\n" + "components:\n" + " schemas:\n" + " test: true\n"); - System.out.println("--------- schema YAML ----------"); + LOGGER.debug("--------- schema YAML ----------"); Yaml31.prettyPrint(openAPI.getComponents().getSchemas().get("test")); assertEquals(Yaml31.pretty(openAPI.getComponents().getSchemas().get("test")), "true\n"); - System.out.println("--------- root 3.0 ----------"); + LOGGER.debug("--------- root 3.0 ----------"); Json.prettyPrint(openAPI); assertEquals(Json.pretty(openAPI), withJacksonSystemLineSeparator("{\n" + " \"openapi\" : \"3.1.0\",\n" + @@ -1422,16 +1425,16 @@ public void testBooleanSchemaSerialization() { " }\n" + " }\n" + "}")); - System.out.println("--------- schema 3.0 ----------"); + LOGGER.debug("--------- schema 3.0 ----------"); Json.prettyPrint(openAPI.getComponents().getSchemas().get("test")); assertEquals(Json.pretty(openAPI.getComponents().getSchemas().get("test")), "{ }"); - System.out.println("--------- root YAML 3.0 ----------"); + LOGGER.debug("--------- root YAML 3.0 ----------"); Yaml.prettyPrint(openAPI); assertEquals(Yaml.pretty(openAPI), "openapi: 3.1.0\n" + "components:\n" + " schemas:\n" + " test: {}\n"); - System.out.println("--------- schema YAML 3.0 ----------"); + LOGGER.debug("--------- schema YAML 3.0 ----------"); Yaml.prettyPrint(openAPI.getComponents().getSchemas().get("test")); assertEquals(Yaml.pretty(openAPI.getComponents().getSchemas().get("test")), "{}\n"); } diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/serialization/properties/PropertySerializationTest.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/serialization/properties/PropertySerializationTest.java index 719b936be1..3025b23a4a 100644 --- a/modules/swagger-core/src/test/java/io/swagger/v3/core/serialization/properties/PropertySerializationTest.java +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/serialization/properties/PropertySerializationTest.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.swagger.v3.core.util.Json; +import io.swagger.v3.core.util.JsonAssert; import io.swagger.v3.oas.models.media.ArraySchema; import io.swagger.v3.oas.models.media.BooleanSchema; import io.swagger.v3.oas.models.media.DateSchema; @@ -32,7 +33,7 @@ public void serializeBooleanSchema() throws IOException { final BooleanSchema p = new BooleanSchema() ._default(true); final String json = "{\"type\":\"boolean\",\"default\":true}"; - assertEquals(m.writeValueAsString(p), json); + JsonAssert.assertJsonEquals(m, m.writeValueAsString(p), json); } @Test(description = "it should deserialize a BooleanSchema") @@ -43,14 +44,14 @@ public void deserializeBooleanSchema() throws IOException { assertNull(p.getFormat()); assertEquals(p.getClass(), BooleanSchema.class); assertEquals(((BooleanSchema) p).getDefault(), Boolean.FALSE); - assertEquals(m.writeValueAsString(p), json); + JsonAssert.assertJsonEquals(m, m.writeValueAsString(p), json); } @Test(description = "it should serialize a DateProperty") public void serializeDateProperty() throws IOException { final DateSchema p = new DateSchema(); final String json = "{\"type\":\"string\",\"format\":\"date\"}"; - assertEquals(m.writeValueAsString(p), json); + JsonAssert.assertJsonEquals(m, m.writeValueAsString(p), json); } @Test(description = "it should deserialize a DateProperty") @@ -60,14 +61,14 @@ public void deserializeDateProperty() throws IOException { assertEquals(p.getType(), "string"); assertEquals(p.getFormat(), "date"); assertEquals(p.getClass(), DateSchema.class); - assertEquals(m.writeValueAsString(p), json); + JsonAssert.assertJsonEquals(m, m.writeValueAsString(p), json); } @Test(description = "it should serialize a DateTimeProperty") public void serializeDateTimeProperty() throws IOException { final DateTimeSchema p = new DateTimeSchema(); final String json = "{\"type\":\"string\",\"format\":\"date-time\"}"; - assertEquals(m.writeValueAsString(p), json); + JsonAssert.assertJsonEquals(m, m.writeValueAsString(p), json); } @Test(description = "it should deserialize a DateTimeProperty") @@ -77,7 +78,7 @@ public void deserializeDateTimeProperty() throws IOException { assertEquals(p.getType(), "string"); assertEquals(p.getFormat(), "date-time"); assertEquals(p.getClass(), DateTimeSchema.class); - assertEquals(m.writeValueAsString(p), json); + JsonAssert.assertJsonEquals(m, m.writeValueAsString(p), json); } @Test(description = "it should serialize a DoubleProperty") @@ -86,7 +87,7 @@ public void serializeDoubleProperty() throws IOException { ._default(new BigDecimal("3.14159")); p.format("double"); final String json = "{\"type\":\"number\",\"format\":\"double\",\"default\":3.14159}"; - assertEquals(m.writeValueAsString(p), json); + JsonAssert.assertJsonEquals(m, m.writeValueAsString(p), json); } @Test(description = "it should deserialize a DoubleProperty") @@ -96,7 +97,7 @@ public void deserializeDoubleProperty() throws IOException { assertEquals(p.getType(), "number"); assertEquals(p.getFormat(), "double"); assertEquals(p.getClass(), NumberSchema.class); - assertEquals(m.writeValueAsString(p), json); + JsonAssert.assertJsonEquals(m, m.writeValueAsString(p), json); } @Test(description = "it should serialize a FloatProperty") @@ -105,7 +106,7 @@ public void serializeFloatProperty() throws IOException { ._default(new BigDecimal("1.2")); p.format("float"); final String json = "{\"type\":\"number\",\"format\":\"float\",\"default\":1.2}"; - assertEquals(m.writeValueAsString(p), json); + JsonAssert.assertJsonEquals(m, m.writeValueAsString(p), json); } @Test(description = "it should deserialize a FloatProperty") @@ -115,7 +116,7 @@ public void deserializeFloatProperty() throws IOException { assertEquals(p.getType(), "number"); assertEquals(p.getFormat(), "float"); assertEquals(p.getClass(), NumberSchema.class); - assertEquals(m.writeValueAsString(p), json); + JsonAssert.assertJsonEquals(m, m.writeValueAsString(p), json); } @Test(description = "it should serialize an IntegerProperty") @@ -123,7 +124,7 @@ public void serializeIntegerProperty() throws IOException { final IntegerSchema p = new IntegerSchema() ._default(32); final String json = "{\"type\":\"integer\",\"format\":\"int32\",\"default\":32}"; - assertEquals(m.writeValueAsString(p), json); + JsonAssert.assertJsonEquals(m, m.writeValueAsString(p), json); } @Test(description = "it should deserialize a IntegerProperty") @@ -133,7 +134,7 @@ public void deserializeIntegerProperty() throws IOException { assertEquals(p.getType(), "integer"); assertEquals(p.getFormat(), "int32"); assertEquals(p.getClass(), IntegerSchema.class); - assertEquals(m.writeValueAsString(p), json); + JsonAssert.assertJsonEquals(m, m.writeValueAsString(p), json); } @Test(description = "it should serialize a LongProperty") @@ -142,7 +143,7 @@ public void serializeLongProperty() throws IOException { .format("int64") ._default(8675309); final String json = "{\"type\":\"integer\",\"format\":\"int64\",\"default\":8675309}"; - assertEquals(m.writeValueAsString(p), json); + JsonAssert.assertJsonEquals(m, m.writeValueAsString(p), json); } @Test(description = "it should deserialize a LongProperty") @@ -152,14 +153,14 @@ public void deserializeLongProperty() throws IOException { assertEquals(p.getType(), "integer"); assertEquals(p.getFormat(), "int64"); assertEquals(p.getClass(), IntegerSchema.class); - assertEquals(m.writeValueAsString(p), json); + JsonAssert.assertJsonEquals(m, m.writeValueAsString(p), json); } @Test(description = "it should serialize a string MapProperty") public void serializeStringMapProperty() throws IOException { final Schema p = new MapSchema().additionalProperties(new StringSchema()); final String json = "{\"type\":\"object\",\"additionalProperties\":{\"type\":\"string\"}}"; - assertEquals(m.writeValueAsString(p), json); + JsonAssert.assertJsonEquals(m, m.writeValueAsString(p), json); } @Test(description = "it should deserialize a string MapProperty") @@ -168,14 +169,14 @@ public void deserializeStringMapProperty() throws IOException { final Schema p = m.readValue(json, Schema.class); assertEquals(p.getType(), "object"); assertEquals(p.getClass(), MapSchema.class); - assertEquals(m.writeValueAsString(p), json); + JsonAssert.assertJsonEquals(m, m.writeValueAsString(p), json); } @Test(description = "it should serialize a integer MapProperty") public void serializeIntegerMapProperty() throws IOException { final Schema p = new MapSchema().additionalProperties(new IntegerSchema()); final String json = "{\"type\":\"object\",\"additionalProperties\":{\"type\":\"integer\",\"format\":\"int32\"}}"; - assertEquals(m.writeValueAsString(p), json); + JsonAssert.assertJsonEquals(m, m.writeValueAsString(p), json); } @Test(description = "it should deserialize a integer MapProperty") @@ -184,14 +185,14 @@ public void deserializeIntegerMapProperty() throws IOException { final Schema p = m.readValue(json, Schema.class); assertEquals(p.getType(), "object"); assertEquals(p.getClass(), MapSchema.class); - assertEquals(m.writeValueAsString(p), json); + JsonAssert.assertJsonEquals(m, m.writeValueAsString(p), json); } @Test(description = "it should serialize a long MapProperty") public void serializeLongMapProperty() throws IOException { final Schema p = new MapSchema().additionalProperties(new IntegerSchema().format("int64")); final String json = "{\"type\":\"object\",\"additionalProperties\":{\"type\":\"integer\",\"format\":\"int64\"}}"; - assertEquals(m.writeValueAsString(p), json); + JsonAssert.assertJsonEquals(m, m.writeValueAsString(p), json); } @Test(description = "it should deserialize a long MapProperty") @@ -200,14 +201,14 @@ public void deserializeLongMapProperty() throws IOException { final Schema p = m.readValue(json, Schema.class); assertEquals(p.getType(), "object"); assertEquals(p.getClass(), MapSchema.class); - assertEquals(m.writeValueAsString(p), json); + JsonAssert.assertJsonEquals(m, m.writeValueAsString(p), json); } @Test(description = "it should serialize a RefProperty") public void serializeRefProperty() throws IOException { final Schema p = new Schema().$ref("#/definitions/Dog"); final String json = "{\"$ref\":\"#/definitions/Dog\"}"; - assertEquals(m.writeValueAsString(p), json); + JsonAssert.assertJsonEquals(m, m.writeValueAsString(p), json); } @Test(description = "it should deserialize a RefProperty") @@ -215,7 +216,7 @@ public void deserializeRefProperty() throws IOException { final String json = "{\"$ref\":\"#/definitions/Dog\"}"; final Schema p = m.readValue(json, Schema.class); assertEquals(p.getClass(), Schema.class); - assertEquals(m.writeValueAsString(p), json); + JsonAssert.assertJsonEquals(m, m.writeValueAsString(p), json); } @Test(description = "it should serialize a StringProperty") @@ -223,7 +224,7 @@ public void serializeStringProperty() throws IOException { final StringSchema p = new StringSchema() ._default("Bob"); final String json = "{\"type\":\"string\",\"default\":\"Bob\"}"; - assertEquals(m.writeValueAsString(p), json); + JsonAssert.assertJsonEquals(m, m.writeValueAsString(p), json); } @Test(description = "it should deserialize a StringProperty") @@ -232,7 +233,7 @@ public void deserializeStringProperty() throws IOException { final Schema p = m.readValue(json, Schema.class); assertEquals(p.getType(), "string"); assertEquals(p.getClass(), StringSchema.class); - assertEquals(m.writeValueAsString(p), json); + JsonAssert.assertJsonEquals(m, m.writeValueAsString(p), json); } @Test(description = "it should serialize a StringProperty with enums") @@ -243,7 +244,7 @@ public void serializeEnumStringProperty() throws IOException { this.add("b"); }}); final String json = "{\"type\":\"string\",\"enum\":[\"a\",\"b\"]}"; - assertEquals(m.writeValueAsString(p), json); + JsonAssert.assertJsonEquals(m, m.writeValueAsString(p), json); } @Test(description = "it should deserialize a StringProperty with enums") @@ -255,7 +256,7 @@ public void deserializeEnumStringProperty() throws IOException { assertNotNull(_enum); assertEquals(_enum, Arrays.asList("a", "b")); assertEquals(p.getClass(), StringSchema.class); - assertEquals(m.writeValueAsString(p), json); + JsonAssert.assertJsonEquals(m, m.writeValueAsString(p), json); } @Test(description = "it should deserialize an IntegerProperty with enums") @@ -267,14 +268,14 @@ public void deserializeEnumIntegerProperty() throws IOException { assertNotNull(_enum); assertEquals(_enum, Arrays.asList(1, 2)); assertEquals(p.getClass(), IntegerSchema.class); - assertEquals(m.writeValueAsString(p), json); + JsonAssert.assertJsonEquals(m, m.writeValueAsString(p), json); } @Test(description = "it should serialize a string array property") public void serializeArrayStringProperty() throws IOException { final Schema p = new ArraySchema().items(new StringSchema()); final String json = "{\"type\":\"array\",\"items\":{\"type\":\"string\"}}"; - assertEquals(m.writeValueAsString(p), json); + JsonAssert.assertJsonEquals(m, m.writeValueAsString(p), json); } @Test(description = "it should deserialize a string array property") @@ -283,14 +284,14 @@ public void deserializeArrayStringProperty() throws IOException { final Schema p = m.readValue(json, Schema.class); assertEquals(p.getType(), "array"); assertEquals(p.getClass(), ArraySchema.class); - assertEquals(m.writeValueAsString(p), json); + JsonAssert.assertJsonEquals(m, m.writeValueAsString(p), json); } @Test(description = "it should serialize a string property with readOnly set") public void serializeReadOnlyStringProperty() throws IOException { final Schema p = new StringSchema().readOnly(true); final String json = "{\"type\":\"string\",\"readOnly\":true}"; - assertEquals(m.writeValueAsString(p), json); + JsonAssert.assertJsonEquals(m, m.writeValueAsString(p), json); } @Test(description = "it should serialize a string property with readOnly unset") @@ -298,7 +299,7 @@ public void deserializeNotReadOnlyStringProperty() throws IOException { final StringSchema p = new StringSchema(); p.setReadOnly(false); final String json = "{\"type\":\"string\",\"readOnly\":false}"; - assertEquals(m.writeValueAsString(p), json); + JsonAssert.assertJsonEquals(m, m.writeValueAsString(p), json); } @Test(description = "it should serialize an object property with required set") @@ -307,7 +308,7 @@ public void serializeObjectPropertyWithRequiredProperties() throws IOException { .addProperties("stringProperty", new StringSchema()); p.required(Arrays.asList("stringProperty")); final String json = "{\"required\":[\"stringProperty\"],\"type\":\"object\",\"properties\":{\"stringProperty\":{\"type\":\"string\"}}}"; - assertEquals(m.writeValueAsString(p), json); + JsonAssert.assertJsonEquals(m, m.writeValueAsString(p), json); } @Test(description = "it should deserialize an object property with required set") diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/util/JsonAssert.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/util/JsonAssert.java new file mode 100644 index 0000000000..72a1870ced --- /dev/null +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/util/JsonAssert.java @@ -0,0 +1,20 @@ +package io.swagger.v3.core.util; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import static org.testng.Assert.assertTrue; + +public final class JsonAssert { + private JsonAssert() { + } + + public static void assertJsonEquals(ObjectMapper mapper, String expectedJson, String actualJson) { + try { + JsonNode expectedNode = mapper.readTree(expectedJson); + JsonNode actualNode = mapper.readTree(actualJson); + assertTrue(expectedNode.equals(actualNode)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} \ No newline at end of file diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/util/OutputReplacer.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/util/OutputReplacer.java index 2e11253784..33348b0092 100644 --- a/modules/swagger-core/src/test/java/io/swagger/v3/core/util/OutputReplacer.java +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/util/OutputReplacer.java @@ -49,12 +49,18 @@ void setOutputStream(PrintStream outputStream) { public String run(Function function) { final PrintStream out = getOutputStream(); final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - + final PrintStream capture = new PrintStream(outputStream); try { - setOutputStream(new PrintStream(outputStream)); + setOutputStream(capture); + PrettyPrintHelper.setOverride(message -> { + capture.println(message); + capture.flush(); + }); function.run(); } finally { + PrettyPrintHelper.clearOverride(); setOutputStream(out); + capture.close(); } try { diff --git a/modules/swagger-eclipse-transformer-maven-plugin/pom.xml b/modules/swagger-eclipse-transformer-maven-plugin/pom.xml index 5b71dd86b1..f686961f26 100644 --- a/modules/swagger-eclipse-transformer-maven-plugin/pom.xml +++ b/modules/swagger-eclipse-transformer-maven-plugin/pom.xml @@ -4,7 +4,7 @@ io.swagger.core.v3 swagger-project - 2.2.34-SNAPSHOT + 2.2.40-SNAPSHOT ../.. 4.0.0 diff --git a/modules/swagger-eclipse-transformer-maven-plugin/src/main/java/io/swagger/v3/oas/transformer/TransformMojo.java b/modules/swagger-eclipse-transformer-maven-plugin/src/main/java/io/swagger/v3/oas/transformer/TransformMojo.java index 14cb87adb3..667f320168 100644 --- a/modules/swagger-eclipse-transformer-maven-plugin/src/main/java/io/swagger/v3/oas/transformer/TransformMojo.java +++ b/modules/swagger-eclipse-transformer-maven-plugin/src/main/java/io/swagger/v3/oas/transformer/TransformMojo.java @@ -1,10 +1,17 @@ package io.swagger.v3.oas.transformer; +import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Consumer; import org.apache.maven.artifact.Artifact; import org.apache.maven.plugin.AbstractMojo; @@ -62,6 +69,8 @@ public class TransformMojo extends AbstractMojo { @Parameter(defaultValue = "transformed") private String classifier; + private static final int MAX_LOG_BUFFER_SIZE = 16 * 1024; + @Parameter(defaultValue = "${project.build.directory}", required = true) private File outputDirectory; @@ -134,7 +143,9 @@ public void transform(final Transformer transformer, final Artifact sourceArtifa * @return A configured transformer */ public Transformer getTransformer() { - final Transformer transformer = new Transformer(System.out, System.err); + final Transformer transformer = new Transformer( + createLoggingPrintStream(getLog()::info), + createLoggingPrintStream(getLog()::error)); transformer.setOptionDefaults(JakartaTransformer.class, getOptionDefaults()); return transformer; } @@ -177,6 +188,55 @@ private Map getOptionDefaults() { return optionDefaults; } + private PrintStream createLoggingPrintStream(Consumer logConsumer) { + try { + return new PrintStream(new LoggingOutputStream(logConsumer, MAX_LOG_BUFFER_SIZE), true, StandardCharsets.UTF_8.name()); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException("UTF-8 encoding is not supported", e); + } + } + + private static final class LoggingOutputStream extends OutputStream { + private final Consumer logConsumer; + private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + private final int maxBufferSize; + + private LoggingOutputStream(Consumer logConsumer, int maxBufferSize) { + this.logConsumer = logConsumer; + this.maxBufferSize = maxBufferSize > 0 ? maxBufferSize : Integer.MAX_VALUE; + } + + @Override + public void write(int b) { + if (b == '\n') { + flushBuffer(); + } else if (b != '\r') { + buffer.write(b); + if (buffer.size() >= this.maxBufferSize) { + flushBuffer(); + } + } + } + + @Override + public void flush() { + flushBuffer(); + } + + @Override + public void close() throws IOException { + flushBuffer(); + } + + private void flushBuffer() { + if (buffer.size() == 0) { + return; + } + logConsumer.accept(new String(buffer.toByteArray(), StandardCharsets.UTF_8)); + buffer.reset(); + } + } + private boolean isEmpty(final String input) { return input == null || input.trim() .length() == 0; diff --git a/modules/swagger-gradle-plugin/README.md b/modules/swagger-gradle-plugin/README.md index ca1116beff..8371a6d0b8 100644 --- a/modules/swagger-gradle-plugin/README.md +++ b/modules/swagger-gradle-plugin/README.md @@ -14,7 +14,7 @@ In many cases it is sufficient to set it to the `classpath` value, like: Alternatively provide as value a classpath with the following dependencies (replacing the provided [`javax` ones](https://github.com/swagger-api/swagger-core/blob/master/modules/swagger-gradle-plugin/src/main/java/io/swagger/v3/plugins/gradle/SwaggerPlugin.java#L16-L25)): -* org.apache.commons:commons-lang3:3.12.0 +* org.apache.commons:commons-lang3:3.18.0 * io.swagger.core.v3:swagger-jaxrs2-jakarta:2.2.9 * jakarta.ws.rs:jakarta.ws.rs-api:3.0.0 * jakarta.servlet:jakarta.servlet-api:5.0.0 @@ -26,7 +26,7 @@ Alternatively provide as value a classpath with the following dependencies (repl ``` plugins { - id "io.swagger.core.v3.swagger-gradle-plugin" version "2.2.33" + id "io.swagger.core.v3.swagger-gradle-plugin" version "2.2.39" } ``` ### Gradle 1.x and 2.0 @@ -43,7 +43,7 @@ buildscript { } } dependencies { - classpath "io.swagger.core.v3:swagger-gradle-plugin:2.2.33" + classpath "io.swagger.core.v3:swagger-gradle-plugin:2.2.39" } } @@ -123,14 +123,13 @@ info: url: http://www.apache.org/licenses/LICENSE-2.0.html ``` -Since version 2.1.6, `sortOutput` parameter is available, allowing to sort object properties and map keys alphabetically. -Since version 2.1.6, `objectMapperProcessorClass` allows to configure also the ObjectMapper instance used to serialize the resolved OpenAPI -Since version 2.1.9, `alwaysResolveAppPath` parameter is available, allowing to trigger resolving of Application Path from annotation also not in runtime (e.g. using servlet in separate application, or in maven plugin at build time, etc) -Since version 2.2.12, `openapi31` parameter is available, if set to true the resolved spec will be processed into a 3.1.0 specification by resolving according to OAS 3.1 rules -Since version 2.1.15, `skipResolveAppPath` parameter is available, allowing to skip resolving of Application Path from annotation -Since version 2.2.17, `defaultResponseCode` parameter is available, allowing to set the code used when resolving responses with no http status code annotation -Since version 2.2.17, `defaultResponseCode` parameter is available, allowing to set the code used when resolving responses with no http status code annotation -Since version 2.2.24, `schemaResolution` parameter is available, allowing to specify how object schemas and object properties within schemas are resolved for OAS 3.0 specification -Since version 2.2.28, `openAPIVersion` parameter is available, allowing to specify the version of the OpenAPI specification to be used for the resolved spec. -Since version 2.2.29, `groupsValidationStrategy` parameter is available, allowing to specify the strategy for resolving Validation annotations (`never`, `always`, `neverIfNoContext`). -Since version 2.2.29, `validatorProcessorClass` parameter is available, allowing to specify a custom validator processor class, implementation of `io.swagger.v3.core.util.ValidatorProcessor`. \ No newline at end of file +- Since version 2.1.6, `sortOutput` parameter is available, allowing to sort object properties and map keys alphabetically. +- Since version 2.1.6, `objectMapperProcessorClass` allows to configure also the ObjectMapper instance used to serialize the resolved OpenAPI +- Since version 2.1.9, `alwaysResolveAppPath` parameter is available, allowing to trigger resolving of Application Path from annotation also not in runtime (e.g. using servlet in separate application, or in maven plugin at build time, etc) +- Since version 2.2.12, `openapi31` parameter is available, if set to true the resolved spec will be processed into a 3.1.0 specification by resolving according to OAS 3.1 rules +- Since version 2.1.15, `skipResolveAppPath` parameter is available, allowing to skip resolving of Application Path from annotation +- Since version 2.2.17, `defaultResponseCode` parameter is available, allowing to set the code used when resolving responses with no http status code annotation +- Since version 2.2.24, `schemaResolution` parameter is available, allowing to specify how object schemas and object properties within schemas are resolved for OAS 3.0 specification +- Since version 2.2.28, `openAPIVersion` parameter is available, allowing to specify the version of the OpenAPI specification to be used for the resolved spec. +- Since version 2.2.29, `groupsValidationStrategy` parameter is available, allowing to specify the strategy for resolving Validation annotations (`never`, `always`, `neverIfNoContext`). +- Since version 2.2.29, `validatorProcessorClass` parameter is available, allowing to specify a custom validator processor class, implementation of `io.swagger.v3.core.util.ValidatorProcessor`. diff --git a/modules/swagger-gradle-plugin/build.gradle b/modules/swagger-gradle-plugin/build.gradle index b9496e55c9..85c953718c 100644 --- a/modules/swagger-gradle-plugin/build.gradle +++ b/modules/swagger-gradle-plugin/build.gradle @@ -24,7 +24,7 @@ compileJava { dependencies { implementation gradleApi() - implementation 'org.apache.commons:commons-lang3:3.14.0' + implementation 'org.apache.commons:commons-lang3:3.18.0' testImplementation gradleTestKit() testImplementation('com.github.tomakehurst:wiremock:2.27.2') { diff --git a/modules/swagger-gradle-plugin/gradle.properties b/modules/swagger-gradle-plugin/gradle.properties index 008428b849..3ffa56989d 100644 --- a/modules/swagger-gradle-plugin/gradle.properties +++ b/modules/swagger-gradle-plugin/gradle.properties @@ -1,2 +1,2 @@ -version=2.2.34-SNAPSHOT +version=2.2.40-SNAPSHOT jettyVersion=9.4.53.v20231009 diff --git a/modules/swagger-gradle-plugin/src/main/java/io/swagger/v3/plugins/gradle/SwaggerPlugin.java b/modules/swagger-gradle-plugin/src/main/java/io/swagger/v3/plugins/gradle/SwaggerPlugin.java index 20458c4bba..781bf346b9 100644 --- a/modules/swagger-gradle-plugin/src/main/java/io/swagger/v3/plugins/gradle/SwaggerPlugin.java +++ b/modules/swagger-gradle-plugin/src/main/java/io/swagger/v3/plugins/gradle/SwaggerPlugin.java @@ -16,8 +16,8 @@ public void apply(Project project) { config.defaultDependencies(new Action() { public void execute(DependencySet dependencies) { - dependencies.add(project.getDependencies().create("org.apache.commons:commons-lang3:3.12.0")); - dependencies.add(project.getDependencies().create("io.swagger.core.v3:swagger-jaxrs2:2.2.34-SNAPSHOT")); + dependencies.add(project.getDependencies().create("org.apache.commons:commons-lang3:3.18.0")); + dependencies.add(project.getDependencies().create("io.swagger.core.v3:swagger-jaxrs2:2.2.40-SNAPSHOT")); dependencies.add(project.getDependencies().create("javax.ws.rs:javax.ws.rs-api:2.1")); dependencies.add(project.getDependencies().create("javax.servlet:javax.servlet-api:3.1.0")); } diff --git a/modules/swagger-gradle-plugin/src/test/java/io/swagger/v3/plugins/gradle/SwaggerResolveTest.java b/modules/swagger-gradle-plugin/src/test/java/io/swagger/v3/plugins/gradle/SwaggerResolveTest.java index 732bb3eb51..b390afd051 100644 --- a/modules/swagger-gradle-plugin/src/test/java/io/swagger/v3/plugins/gradle/SwaggerResolveTest.java +++ b/modules/swagger-gradle-plugin/src/test/java/io/swagger/v3/plugins/gradle/SwaggerResolveTest.java @@ -77,7 +77,7 @@ public void testSwaggerResolveTask() throws IOException { " mavenCentral()\n" + "}\n" + "dependencies { \n" + - " implementation 'io.swagger.core.v3:swagger-jaxrs2:2.2.34-SNAPSHOT'\n" + + " implementation 'io.swagger.core.v3:swagger-jaxrs2:2.2.40-SNAPSHOT'\n" + " implementation 'javax.ws.rs:javax.ws.rs-api:2.1'\n" + " implementation 'javax.servlet:javax.servlet-api:3.1.0'\n" + " testImplementation 'com.github.tomakehurst:wiremock:2.27.2'\n" + @@ -150,7 +150,7 @@ public void testSwaggerResolveWithOAS31OptionTask() throws IOException { " mavenCentral()\n" + "}\n" + "dependencies { \n" + - " implementation 'io.swagger.core.v3:swagger-jaxrs2:2.2.34-SNAPSHOT'\n" + + " implementation 'io.swagger.core.v3:swagger-jaxrs2:2.2.40-SNAPSHOT'\n" + " implementation 'javax.ws.rs:javax.ws.rs-api:2.1'\n" + " implementation 'javax.servlet:javax.servlet-api:3.1.0'\n" + " testImplementation 'com.github.tomakehurst:wiremock:2.27.2'\n" + diff --git a/modules/swagger-integration/pom.xml b/modules/swagger-integration/pom.xml index 3291022e26..43634e770e 100644 --- a/modules/swagger-integration/pom.xml +++ b/modules/swagger-integration/pom.xml @@ -6,7 +6,7 @@ io.swagger.core.v3 swagger-project - 2.2.34-SNAPSHOT + 2.2.40-SNAPSHOT ../.. swagger-integration diff --git a/modules/swagger-java17-support/pom.xml b/modules/swagger-java17-support/pom.xml index 4c91a741c3..6046d18c21 100644 --- a/modules/swagger-java17-support/pom.xml +++ b/modules/swagger-java17-support/pom.xml @@ -5,7 +5,7 @@ io.swagger.core.v3 swagger-project - 2.2.34-SNAPSHOT + 2.2.40-SNAPSHOT ../../pom.xml swagger-java17-support diff --git a/modules/swagger-java17-support/src/test/java/io/swagger/v3/java17/resolving/SwaggerTestBase.java b/modules/swagger-java17-support/src/test/java/io/swagger/v3/java17/resolving/SwaggerTestBase.java index 2735cb788a..427895be63 100644 --- a/modules/swagger-java17-support/src/test/java/io/swagger/v3/java17/resolving/SwaggerTestBase.java +++ b/modules/swagger-java17-support/src/test/java/io/swagger/v3/java17/resolving/SwaggerTestBase.java @@ -6,9 +6,12 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import io.swagger.v3.core.jackson.ModelResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public abstract class SwaggerTestBase { static ObjectMapper mapper; + private static final Logger LOGGER = LoggerFactory.getLogger(SwaggerTestBase.class); public static ObjectMapper mapper() { if (mapper == null) { @@ -26,9 +29,9 @@ protected ModelResolver modelResolver() { protected void prettyPrint(Object o) { try { - System.out.println(mapper().writer(new DefaultPrettyPrinter()).writeValueAsString(o)); + LOGGER.debug(mapper().writer(new DefaultPrettyPrinter()).writeValueAsString(o)); } catch (Exception e) { - e.printStackTrace(); + LOGGER.error("Failed to pretty print object", e); } } } diff --git a/modules/swagger-jaxrs2-servlet-initializer-v2/pom.xml b/modules/swagger-jaxrs2-servlet-initializer-v2/pom.xml index ba18af003f..7d295b9278 100644 --- a/modules/swagger-jaxrs2-servlet-initializer-v2/pom.xml +++ b/modules/swagger-jaxrs2-servlet-initializer-v2/pom.xml @@ -5,7 +5,7 @@ swagger-project io.swagger.core.v3 - 2.2.34-SNAPSHOT + 2.2.40-SNAPSHOT ../../ 4.0.0 diff --git a/modules/swagger-jaxrs2-servlet-initializer/pom.xml b/modules/swagger-jaxrs2-servlet-initializer/pom.xml index 596360a231..837915efd4 100644 --- a/modules/swagger-jaxrs2-servlet-initializer/pom.xml +++ b/modules/swagger-jaxrs2-servlet-initializer/pom.xml @@ -5,7 +5,7 @@ swagger-project io.swagger.core.v3 - 2.2.34-SNAPSHOT + 2.2.40-SNAPSHOT ../../ 4.0.0 diff --git a/modules/swagger-jaxrs2/pom.xml b/modules/swagger-jaxrs2/pom.xml index f8cdcbeb64..eed807e5a3 100644 --- a/modules/swagger-jaxrs2/pom.xml +++ b/modules/swagger-jaxrs2/pom.xml @@ -5,7 +5,7 @@ swagger-project io.swagger.core.v3 - 2.2.34-SNAPSHOT + 2.2.40-SNAPSHOT ../../ 4.0.0 diff --git a/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/integration/OpenApiServlet.java b/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/integration/OpenApiServlet.java index 8c7dbe38d4..a246f1dc99 100644 --- a/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/integration/OpenApiServlet.java +++ b/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/integration/OpenApiServlet.java @@ -41,7 +41,7 @@ public void init(ServletConfig config) throws ServletException { .ctxId(ctxId) .buildContext(true); } catch (OpenApiConfigurationException e) { - e.printStackTrace(); + LOGGER.error("Failed to initialize OpenAPI servlet context", e); } } diff --git a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/PetResourceTest.java b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/PetResourceTest.java index 7d003965dc..1cd25fcbdb 100644 --- a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/PetResourceTest.java +++ b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/PetResourceTest.java @@ -54,6 +54,8 @@ import io.swagger.v3.jaxrs2.petstore.tags.TagOperationResource; import io.swagger.v3.oas.integration.SwaggerConfiguration; import io.swagger.v3.oas.models.OpenAPI; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.testng.annotations.Test; import java.io.File; @@ -73,6 +75,7 @@ * Adding a lot of tests of different pet resource examples */ public class PetResourceTest extends AbstractAnnotationTest { + private static final Logger LOGGER = LoggerFactory.getLogger(PetResourceTest.class); private static final String PETSTORE_SOURCE = "petstore/"; private static final String TAGS_SOURCE = "petstore/tags/"; private static final String OPERATIONS_SOURCE = "petstore/operation/"; @@ -314,7 +317,7 @@ private void compare(final Class clazz, final String source, boolean openapi31) compareAsYaml(clazz, getOpenAPIAsString(file)); } } catch (IOException e) { - e.printStackTrace(); + LOGGER.error("Failed to compare class {} with YAML resource {}", clazz.getName(), file, e); fail(); } } diff --git a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/annotations/AbstractAnnotationTest.java b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/annotations/AbstractAnnotationTest.java index 8048810680..1ea6863dc3 100644 --- a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/annotations/AbstractAnnotationTest.java +++ b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/annotations/AbstractAnnotationTest.java @@ -8,6 +8,8 @@ import io.swagger.v3.oas.integration.SwaggerConfiguration; import io.swagger.v3.oas.models.OpenAPI; import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; @@ -16,6 +18,8 @@ import static org.testng.Assert.fail; public abstract class AbstractAnnotationTest { + private static final Logger LOGGER = LoggerFactory.getLogger(AbstractAnnotationTest.class); + public String readIntoYaml(final Class cls) { Reader reader = new Reader(new OpenAPI()); OpenAPI openAPI = reader.read(cls); @@ -37,7 +41,7 @@ public void compareToYamlFile(final Class cls, String source){ try { compareAsYaml(cls, getOpenAPIAsString(file)); } catch (IOException e) { - e.printStackTrace(); + LOGGER.error("Failed to compare class {} with YAML resource {}", cls.getName(), file, e); fail(); } } diff --git a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/util/ServletUtilsTest.java b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/util/ServletUtilsTest.java index 57af4fa7d0..b53f61c513 100644 --- a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/util/ServletUtilsTest.java +++ b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/util/ServletUtilsTest.java @@ -10,6 +10,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; import static org.testng.Assert.assertNotNull; public class ServletUtilsTest { @@ -25,7 +26,8 @@ public void convertWithRightOutputSize() throws Exception { assertEquals(multivaluedMap.size(), 2); assertEquals(multivaluedMap.get("key1").size(), 2); assertEquals(multivaluedMap.get("key2").size(), 4); - assertEquals(multivaluedMap.keySet().iterator().next(), "key1"); + assertTrue(multivaluedMap.containsKey("key1")); + assertTrue(multivaluedMap.containsKey("key2")); } @Test(description = "convert query parameters to multivaluedmap with decoded values") diff --git a/modules/swagger-maven-plugin/README.md b/modules/swagger-maven-plugin/README.md index a368752459..6c9f45c029 100644 --- a/modules/swagger-maven-plugin/README.md +++ b/modules/swagger-maven-plugin/README.md @@ -22,7 +22,7 @@ Both `javax` and `jakarta` examples are provided below io.swagger.core.v3 swagger-maven-plugin - 2.2.33 + 2.2.39 openapi ${project.build.directory}/generatedtest @@ -47,7 +47,7 @@ Both `javax` and `jakarta` examples are provided below io.swagger.core.v3 swagger-jaxrs2 - 2.2.33 + 2.2.39 @@ -73,7 +73,7 @@ Both `javax` and `jakarta` examples are provided below io.swagger.core.v3 swagger-maven-plugin-jakarta - 2.2.33 + 2.2.39 openapi ${project.build.directory}/generatedtest @@ -98,7 +98,7 @@ Both `javax` and `jakarta` examples are provided below io.swagger.core.v3 swagger-jaxrs2-jakarta - 2.2.33 + 2.2.39 @@ -126,7 +126,7 @@ Both `javax` and `jakarta` examples are provided below io.swagger.core.v3 swagger-maven-plugin - 2.2.33 + 2.2.39 openapi ${project.build.directory}/generatedtest @@ -155,7 +155,7 @@ Both `javax` and `jakarta` examples are provided below io.swagger.core.v3 swagger-maven-plugin-jakarta - 2.2.33 + 2.2.39 openapi ${project.build.directory}/generatedtest diff --git a/modules/swagger-maven-plugin/pom.xml b/modules/swagger-maven-plugin/pom.xml index 4cb69b5b1d..288fb3149b 100644 --- a/modules/swagger-maven-plugin/pom.xml +++ b/modules/swagger-maven-plugin/pom.xml @@ -4,7 +4,7 @@ io.swagger.core.v3 swagger-project - 2.2.34-SNAPSHOT + 2.2.40-SNAPSHOT ../.. 4.0.0 diff --git a/modules/swagger-maven-plugin/src/main/java/io/swagger/v3/plugin/maven/IncludeProjectDependenciesComponentConfigurator.java b/modules/swagger-maven-plugin/src/main/java/io/swagger/v3/plugin/maven/IncludeProjectDependenciesComponentConfigurator.java index 8c37bbcd2c..40523ce96a 100644 --- a/modules/swagger-maven-plugin/src/main/java/io/swagger/v3/plugin/maven/IncludeProjectDependenciesComponentConfigurator.java +++ b/modules/swagger-maven-plugin/src/main/java/io/swagger/v3/plugin/maven/IncludeProjectDependenciesComponentConfigurator.java @@ -39,8 +39,9 @@ public void configureComponent(Object component, PlexusConfiguration configurati private void addProjectDependenciesToClassRealm(ExpressionEvaluator expressionEvaluator, ClassRealm containerRealm) throws ComponentConfigurationException { List compileClasspathElements; try { - //noinspection unchecked - compileClasspathElements = (List) expressionEvaluator.evaluate("${project.compileClasspathElements}"); + @SuppressWarnings("unchecked") + List evaluated = (List) expressionEvaluator.evaluate("${project.compileClasspathElements}"); + compileClasspathElements = evaluated; } catch (ExpressionEvaluationException e) { throw new ComponentConfigurationException("There was a problem evaluating: ${project.compileClasspathElements}", e); } @@ -68,4 +69,4 @@ private URL[] buildURLs(List runtimeClasspathElements) throws ComponentC return urls.toArray(new URL[urls.size()]); } -} \ No newline at end of file +} diff --git a/modules/swagger-models/pom.xml b/modules/swagger-models/pom.xml index bb00424253..4857479d5b 100644 --- a/modules/swagger-models/pom.xml +++ b/modules/swagger-models/pom.xml @@ -4,7 +4,7 @@ io.swagger.core.v3 swagger-project - 2.2.34-SNAPSHOT + 2.2.40-SNAPSHOT ../.. 4.0.0 diff --git a/modules/swagger-models/src/main/java/io/swagger/v3/oas/models/media/ComposedSchema.java b/modules/swagger-models/src/main/java/io/swagger/v3/oas/models/media/ComposedSchema.java index deea389061..a1a9e222d6 100644 --- a/modules/swagger-models/src/main/java/io/swagger/v3/oas/models/media/ComposedSchema.java +++ b/modules/swagger-models/src/main/java/io/swagger/v3/oas/models/media/ComposedSchema.java @@ -6,6 +6,55 @@ public class ComposedSchema extends Schema { + public static ComposedSchema from(Schema subtypeModel) { + ComposedSchema composedSchema = new ComposedSchema(); + copySchemaProperties(composedSchema, subtypeModel); + + + if (shouldSetExample(subtypeModel)) { + composedSchema.example(subtypeModel.getExample()); + } + composedSchema.setEnum(subtypeModel.getEnum()); + return composedSchema; + } + + private static void copySchemaProperties(ComposedSchema target, Schema source) { + target.title(source.getTitle()) + .name(source.getName()) + .deprecated(source.getDeprecated()) + .additionalProperties(source.getAdditionalProperties()) + .description(source.getDescription()) + .discriminator(source.getDiscriminator()) + .exclusiveMaximum(source.getExclusiveMaximum()) + .exclusiveMinimum(source.getExclusiveMinimum()) + .externalDocs(source.getExternalDocs()) + .format(source.getFormat()) + .maximum(source.getMaximum()) + .maxItems(source.getMaxItems()) + .maxLength(source.getMaxLength()) + .maxProperties(source.getMaxProperties()) + .minimum(source.getMinimum()) + .minItems(source.getMinItems()) + .minLength(source.getMinLength()) + .minProperties(source.getMinProperties()) + .multipleOf(source.getMultipleOf()) + .not(source.getNot()) + .nullable(source.getNullable()) + .pattern(source.getPattern()) + .properties(source.getProperties()) + .readOnly(source.getReadOnly()) + .required(source.getRequired()) + .type(source.getType()) + .uniqueItems(source.getUniqueItems()) + .writeOnly(source.getWriteOnly()) + .xml(source.getXml()) + .extensions(source.getExtensions()); + } + + + private static boolean shouldSetExample(Schema model) { + return model.getExample() != null || model.getExampleSetFlag(); + } @Override public String toString() { diff --git a/modules/swagger-models/src/main/java/io/swagger/v3/oas/models/media/Schema.java b/modules/swagger-models/src/main/java/io/swagger/v3/oas/models/media/Schema.java index fe4f335bfc..c1653fd8f9 100644 --- a/modules/swagger-models/src/main/java/io/swagger/v3/oas/models/media/Schema.java +++ b/modules/swagger-models/src/main/java/io/swagger/v3/oas/models/media/Schema.java @@ -2155,8 +2155,8 @@ public boolean equals(java.lang.Object o) { Objects.equals(this.$comment, schema.$comment) && Objects.equals(this.examples, schema.examples) && Objects.equals(this.prefixItems, schema.prefixItems) && - Objects.equals(this.items, schema.items) - + Objects.equals(this.items, schema.items) && + Objects.equals(this.booleanSchemaValue, schema.booleanSchemaValue) ; } @@ -2169,7 +2169,7 @@ public int hashCode() { discriminator, _enum, _default, patternProperties, $id, $anchor, $schema, $vocabulary, $dynamicAnchor, $dynamicRef, types, allOf, anyOf, oneOf, _const, contentEncoding, contentMediaType, contentSchema, propertyNames, unevaluatedProperties, maxContains, minContains, additionalItems, unevaluatedItems, - _if, _else, then, dependentRequired, dependentSchemas, $comment, examples, prefixItems, items); + _if, _else, then, dependentRequired, dependentSchemas, $comment, examples, prefixItems, items, booleanSchemaValue); } public java.util.Map getExtensions() { @@ -2258,6 +2258,7 @@ public String toString() { sb.append(" dependentSchemas: ").append(toIndentedString(dependentSchemas)).append("\n"); sb.append(" $comment: ").append(toIndentedString($comment)).append("\n"); sb.append(" prefixItems: ").append(toIndentedString(prefixItems)).append("\n"); + sb.append(" booleanSchemaValue").append(toIndentedString(booleanSchemaValue)).append("\n"); } sb.append("}"); return sb.toString(); diff --git a/modules/swagger-models/src/main/java/io/swagger/v3/oas/models/parameters/Parameter.java b/modules/swagger-models/src/main/java/io/swagger/v3/oas/models/parameters/Parameter.java index e9242cb2ea..e58c7be715 100644 --- a/modules/swagger-models/src/main/java/io/swagger/v3/oas/models/parameters/Parameter.java +++ b/modules/swagger-models/src/main/java/io/swagger/v3/oas/models/parameters/Parameter.java @@ -321,7 +321,10 @@ public Parameter content(Content content) { } public void set$ref(String $ref) { - if ($ref != null && ($ref.indexOf('.') == -1 && $ref.indexOf('/') == -1)) { + if ($ref != null && + !$ref.startsWith("#/components/parameters/") + && $ref.indexOf('.') == -1 + && $ref.indexOf('/') == -1) { $ref = "#/components/parameters/" + $ref; } this.$ref = $ref; diff --git a/modules/swagger-models/src/test/java/io/swagger/test/SimpleBuilderTest.java b/modules/swagger-models/src/test/java/io/swagger/test/SimpleBuilderTest.java index 45f82dbea1..2368f792c3 100644 --- a/modules/swagger-models/src/test/java/io/swagger/test/SimpleBuilderTest.java +++ b/modules/swagger-models/src/test/java/io/swagger/test/SimpleBuilderTest.java @@ -23,6 +23,8 @@ import io.swagger.v3.oas.models.responses.ApiResponse; import io.swagger.v3.oas.models.responses.ApiResponses; import io.swagger.v3.oas.models.tags.Tag; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.testng.annotations.Test; import java.math.BigDecimal; @@ -31,6 +33,8 @@ import java.util.Map; public class SimpleBuilderTest { + private static final Logger LOGGER = LoggerFactory.getLogger(SimpleBuilderTest.class); + @Test public void testBuilder() throws Exception { // basic metadata @@ -119,7 +123,7 @@ public void testBuilder() throws Exception { ) ); - System.out.println(writeJson(oai)); + LOGGER.debug(writeJson(oai)); } public static String writeJson(Object value) throws Exception { diff --git a/modules/swagger-models/src/test/java/io/swagger/v3/oas/models/media/SchemaTest.java b/modules/swagger-models/src/test/java/io/swagger/v3/oas/models/media/SchemaTest.java new file mode 100644 index 0000000000..a3e8a7ec54 --- /dev/null +++ b/modules/swagger-models/src/test/java/io/swagger/v3/oas/models/media/SchemaTest.java @@ -0,0 +1,181 @@ +package io.swagger.v3.oas.models.media; + +import org.testng.annotations.Test; +import static org.testng.Assert.*; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +public class SchemaTest { + + @Test + public void testEqualsWithDifferentBooleanSchemaValue() { + Schema schema1 = new Schema<>(); + Schema schema2 = new Schema<>(); + + schema1.setBooleanSchemaValue(true); + schema2.setBooleanSchemaValue(false); + + assertFalse(schema1.equals(schema2)); + assertFalse(schema2.equals(schema1)); + assertNotEquals(schema1.hashCode(), schema2.hashCode()); + } + + + @Test + public void testEqualsWithOneNullBooleanSchemaValue() { + Schema schema1 = new Schema<>(); + Schema schema2 = new Schema<>(); + + schema1.setBooleanSchemaValue(true); + schema2.setBooleanSchemaValue(null); + + assertFalse(schema1.equals(schema2)); + assertFalse(schema2.equals(schema1)); + assertNotEquals(schema1.hashCode(), schema2.hashCode()); + } + + @Test + public void testBooleanSchemaValueFalseVsTrue() { + // This test specifically addresses the issue where false gets mapped to true + Schema schemaFalse = new Schema<>(); + Schema schemaTrue = new Schema<>(); + + schemaFalse.setBooleanSchemaValue(false); + schemaTrue.setBooleanSchemaValue(true); + + // These should be different + assertFalse(schemaFalse.equals(schemaTrue)); + assertFalse(schemaTrue.equals(schemaFalse)); + + // Hash codes should be different to prevent collisions in maps + assertNotEquals(schemaFalse.hashCode(), schemaTrue.hashCode()); + + // Verify the actual values + assertEquals(Boolean.FALSE, schemaFalse.getBooleanSchemaValue()); + assertEquals(Boolean.TRUE, schemaTrue.getBooleanSchemaValue()); + } + + @Test + public void testComplexSchemaEqualsWithBooleanSchemaValue() { + Schema schema1 = createComplexSchema(); + Schema schema2 = createComplexSchema(); + + // Initially they should be equal + assertTrue(schema1.equals(schema2)); + assertEquals(schema1.hashCode(), schema2.hashCode()); + + // Change only booleanSchemaValue + schema2.setBooleanSchemaValue(false); + + // Now they should be different + assertFalse(schema1.equals(schema2)); + assertNotEquals(schema1.hashCode(), schema2.hashCode()); + } + + + @Test + public void testBooleanSchemaValueInMapLookup() { + Map, String> visitedMap = new HashMap<>(); + + Schema schemaTrue = new Schema<>(); + Schema schemaFalse = new Schema<>(); + + schemaTrue.setBooleanSchemaValue(true); + schemaFalse.setBooleanSchemaValue(false); + + visitedMap.put(schemaTrue, "true_schema"); + visitedMap.put(schemaFalse, "false_schema"); + + assertEquals(visitedMap.size(), 2); + assertEquals(visitedMap.get(schemaTrue), "true_schema"); + assertEquals(visitedMap.get(schemaFalse), "false_schema"); + + Schema anotherTrue = new Schema<>(); + anotherTrue.setBooleanSchemaValue(true); + assertEquals(visitedMap.get(anotherTrue), "true_schema"); + + Schema anotherFalse = new Schema<>(); + anotherFalse.setBooleanSchemaValue(false); + assertEquals(visitedMap.get(anotherFalse), "false_schema"); + } + + @Test + public void testSchemaResolutionWithBooleanSchemaValue() { + Map, Schema> visitedMap = new HashMap<>(); + + // Create a schema with booleanSchemaValue = false + Schema originalSchema = new Schema<>(); + originalSchema.setBooleanSchemaValue(false); + originalSchema.setTitle("Original Schema"); + originalSchema.setType("boolean"); + + // Create a resolved version of the same schema + Schema resolvedSchema = new Schema<>(); + resolvedSchema.setBooleanSchemaValue(false); + resolvedSchema.setTitle("Resolved Schema"); + resolvedSchema.setType("boolean"); + + // Put the original schema in the visited map + visitedMap.put(originalSchema, resolvedSchema); + + // Create another schema with identical structure but booleanSchemaValue = true + Schema differentSchema = new Schema<>(); + differentSchema.setBooleanSchemaValue(true); + differentSchema.setTitle("Original Schema"); + differentSchema.setType("boolean"); + + // The lookup should NOT return the resolved schema for the different schema + assertNull(visitedMap.get(differentSchema)); + + // But it should return the resolved schema for an equivalent schema with false value + Schema equivalentSchema = new Schema<>(); + equivalentSchema.setBooleanSchemaValue(false); + equivalentSchema.setTitle("Original Schema"); + equivalentSchema.setType("boolean"); + assertEquals(resolvedSchema, visitedMap.get(equivalentSchema)); + + // Verify that the schemas with different booleanSchemaValue are indeed different + assertNotEquals(originalSchema, differentSchema); + assertNotEquals(originalSchema.hashCode(), differentSchema.hashCode()); + } + + @Test + public void testBooleanSchemaValueInSetOperations() { + java.util.Set> schemaSet = new java.util.HashSet<>(); + + Schema schemaTrue = new Schema<>(); + schemaTrue.setBooleanSchemaValue(true); + + Schema schemaFalse = new Schema<>(); + schemaFalse.setBooleanSchemaValue(false); + + schemaSet.add(schemaTrue); + schemaSet.add(schemaFalse); + + // Should have two distinct schemas in the set + assertEquals(schemaSet.size(), 2); + assertTrue(schemaSet.contains(schemaTrue)); + assertTrue(schemaSet.contains(schemaFalse)); + + // Adding equivalent schemas should not increase the set size + Schema anotherTrue = new Schema<>(); + anotherTrue.setBooleanSchemaValue(true); + schemaSet.add(anotherTrue); + assertEquals(schemaSet.size(), 2); + } + + private Schema createComplexSchema() { + Schema schema = new Schema<>(); + schema.setTitle("Complex Schema"); + schema.setType("object"); + schema.setDescription("A complex schema for testing"); + schema.setMaximum(new BigDecimal("100")); + schema.setMinimum(new BigDecimal("0")); + schema.setRequired(Arrays.asList("id", "name")); + schema.setBooleanSchemaValue(true); + return schema; + } +} diff --git a/modules/swagger-project-jakarta/modules/swagger-annotations-jakarta/pom.xml b/modules/swagger-project-jakarta/modules/swagger-annotations-jakarta/pom.xml index 991446e8c6..0814855ffd 100644 --- a/modules/swagger-project-jakarta/modules/swagger-annotations-jakarta/pom.xml +++ b/modules/swagger-project-jakarta/modules/swagger-annotations-jakarta/pom.xml @@ -4,7 +4,7 @@ io.swagger.core.v3 swagger-project-jakarta - 2.2.34-SNAPSHOT + 2.2.40-SNAPSHOT ../.. 4.0.0 diff --git a/modules/swagger-project-jakarta/modules/swagger-core-jakarta/pom.xml b/modules/swagger-project-jakarta/modules/swagger-core-jakarta/pom.xml index a41a0c0cb5..a103eb1867 100644 --- a/modules/swagger-project-jakarta/modules/swagger-core-jakarta/pom.xml +++ b/modules/swagger-project-jakarta/modules/swagger-core-jakarta/pom.xml @@ -4,7 +4,7 @@ io.swagger.core.v3 swagger-project-jakarta - 2.2.34-SNAPSHOT + 2.2.40-SNAPSHOT ../.. 4.0.0 diff --git a/modules/swagger-project-jakarta/modules/swagger-integration-jakarta/pom.xml b/modules/swagger-project-jakarta/modules/swagger-integration-jakarta/pom.xml index d37a59d0af..d9ae9a012e 100644 --- a/modules/swagger-project-jakarta/modules/swagger-integration-jakarta/pom.xml +++ b/modules/swagger-project-jakarta/modules/swagger-integration-jakarta/pom.xml @@ -4,7 +4,7 @@ io.swagger.core.v3 swagger-project-jakarta - 2.2.34-SNAPSHOT + 2.2.40-SNAPSHOT ../.. 4.0.0 diff --git a/modules/swagger-project-jakarta/modules/swagger-jaxrs2-jakarta/pom.xml b/modules/swagger-project-jakarta/modules/swagger-jaxrs2-jakarta/pom.xml index 9c542cee1c..13e9516e17 100644 --- a/modules/swagger-project-jakarta/modules/swagger-jaxrs2-jakarta/pom.xml +++ b/modules/swagger-project-jakarta/modules/swagger-jaxrs2-jakarta/pom.xml @@ -4,7 +4,7 @@ io.swagger.core.v3 swagger-project-jakarta - 2.2.34-SNAPSHOT + 2.2.40-SNAPSHOT ../.. 4.0.0 diff --git a/modules/swagger-project-jakarta/modules/swagger-jaxrs2-servlet-initializer-jakarta/pom.xml b/modules/swagger-project-jakarta/modules/swagger-jaxrs2-servlet-initializer-jakarta/pom.xml index cdcf2db2a5..e92762c48d 100644 --- a/modules/swagger-project-jakarta/modules/swagger-jaxrs2-servlet-initializer-jakarta/pom.xml +++ b/modules/swagger-project-jakarta/modules/swagger-jaxrs2-servlet-initializer-jakarta/pom.xml @@ -4,7 +4,7 @@ io.swagger.core.v3 swagger-project-jakarta - 2.2.34-SNAPSHOT + 2.2.40-SNAPSHOT ../.. 4.0.0 diff --git a/modules/swagger-project-jakarta/modules/swagger-jaxrs2-servlet-initializer-v2-jakarta/pom.xml b/modules/swagger-project-jakarta/modules/swagger-jaxrs2-servlet-initializer-v2-jakarta/pom.xml index 5835ee93cf..93adbbfbe9 100644 --- a/modules/swagger-project-jakarta/modules/swagger-jaxrs2-servlet-initializer-v2-jakarta/pom.xml +++ b/modules/swagger-project-jakarta/modules/swagger-jaxrs2-servlet-initializer-v2-jakarta/pom.xml @@ -4,7 +4,7 @@ io.swagger.core.v3 swagger-project-jakarta - 2.2.34-SNAPSHOT + 2.2.40-SNAPSHOT ../.. 4.0.0 diff --git a/modules/swagger-project-jakarta/modules/swagger-maven-plugin-jakarta/pom.xml b/modules/swagger-project-jakarta/modules/swagger-maven-plugin-jakarta/pom.xml index e85921a570..642003697b 100644 --- a/modules/swagger-project-jakarta/modules/swagger-maven-plugin-jakarta/pom.xml +++ b/modules/swagger-project-jakarta/modules/swagger-maven-plugin-jakarta/pom.xml @@ -4,7 +4,7 @@ io.swagger.core.v3 swagger-project-jakarta - 2.2.34-SNAPSHOT + 2.2.40-SNAPSHOT ../.. 4.0.0 diff --git a/modules/swagger-project-jakarta/modules/swagger-maven-plugin-jakarta/src/main/java/io/swagger/v3/plugin/maven/jakarta/JakartaTransformer.java b/modules/swagger-project-jakarta/modules/swagger-maven-plugin-jakarta/src/main/java/io/swagger/v3/plugin/maven/jakarta/JakartaTransformer.java index 832bad509b..a95022306f 100644 --- a/modules/swagger-project-jakarta/modules/swagger-maven-plugin-jakarta/src/main/java/io/swagger/v3/plugin/maven/jakarta/JakartaTransformer.java +++ b/modules/swagger-project-jakarta/modules/swagger-maven-plugin-jakarta/src/main/java/io/swagger/v3/plugin/maven/jakarta/JakartaTransformer.java @@ -1,5 +1,8 @@ package io.swagger.v3.plugin.maven.jakarta; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.BufferedWriter; @@ -42,6 +45,8 @@ */ public class JakartaTransformer { + private static final Logger LOGGER = LoggerFactory.getLogger(JakartaTransformer.class); + /* * Substitution tokens + regex @@ -212,7 +217,7 @@ public FileVisitResult visitFile(Path file, Files.copy(file, pathInZipfile, options); } catch (IOException e) { - e.printStackTrace(); + LOGGER.error("Error copying file '{}' into ZIP filesystem", file, e); } return FileVisitResult.CONTINUE; diff --git a/modules/swagger-project-jakarta/modules/swagger-models-jakarta/pom.xml b/modules/swagger-project-jakarta/modules/swagger-models-jakarta/pom.xml index bf0eae155d..32539a46f2 100644 --- a/modules/swagger-project-jakarta/modules/swagger-models-jakarta/pom.xml +++ b/modules/swagger-project-jakarta/modules/swagger-models-jakarta/pom.xml @@ -4,7 +4,7 @@ io.swagger.core.v3 swagger-project-jakarta - 2.2.34-SNAPSHOT + 2.2.40-SNAPSHOT ../.. 4.0.0 diff --git a/modules/swagger-project-jakarta/pom.xml b/modules/swagger-project-jakarta/pom.xml index 240adaefbd..282ae35a1a 100644 --- a/modules/swagger-project-jakarta/pom.xml +++ b/modules/swagger-project-jakarta/pom.xml @@ -6,7 +6,7 @@ pom swagger-project-jakarta swagger-project-jakarta - 2.2.34-SNAPSHOT + 2.2.40-SNAPSHOT https://github.com/swagger-api/swagger-core scm:git:git@github.com:swagger-api/swagger-core.git @@ -471,13 +471,13 @@ 6.4.0 5.0.0 3.1.10 - 2.18.2 - 2.18.2 + 2.19.2 + 2.19.2 1.5.16 - 4.8.179 + 4.8.184 32.1.3-jre 1.10.15 - 3.17.0 + 3.18.0 2.18.0 2.0.9 9.4.56.v20240826g diff --git a/pom.xml b/pom.xml index 764ad838ae..ccbddc10c9 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ pom swagger-project swagger-project - 2.2.34-SNAPSHOT + 2.2.40-SNAPSHOT https://github.com/swagger-api/swagger-core scm:git:git@github.com:swagger-api/swagger-core.git @@ -628,13 +628,13 @@ 6.4.0 4.0.4 2.46 - 2.18.2 - 2.18.2 + 2.19.2 + 2.19.2 1.5.16 - 4.8.179 + 4.8.184 32.1.3-jre 1.10.15 - 3.17.0 + 3.18.0 2.18.0 2.0.9 9.4.56.v20240826