Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,36 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context
return context.resolve(aType);
}

// Special handling for java.util.stream.Stream - treat it as an array type
if (isStreamType(type)) {
JavaType valueType = type.getContentType();
if (valueType != null) {
Schema items = context.resolve(new AnnotatedType()
.type(valueType)
.schemaProperty(annotatedType.isSchemaProperty())
.ctxAnnotations(annotatedType.getCtxAnnotations())
.skipSchemaName(true)
.resolveAsRef(annotatedType.isResolveAsRef())
.propertyName(annotatedType.getPropertyName())
.jsonViewAnnotation(annotatedType.getJsonViewAnnotation())
.components(annotatedType.getComponents())
.resolveEnumAsRef(annotatedType.isResolveEnumAsRef())
.parent(annotatedType.getParent()));

if (items != null) {
Schema arrayModel = new ArraySchema().items(items);
if (openapi31) {
arrayModel.specVersion(SpecVersion.V31);
}
arrayModel.name(name);
if (resolvedArrayAnnotation != null) {
resolveArraySchema(annotatedType, (ArraySchema) arrayModel, resolvedArrayAnnotation);
}
return arrayModel;
}
}
}

if (type.isContainerType()) {
// TODO currently a MapSchema or ArraySchema don't also support composed schema props (oneOf,..)
hasCompositionKeywords = false;
Expand Down Expand Up @@ -529,7 +559,8 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context
mapModel.name(name);
model = mapModel;
} else if (valueType != null) {
if (ReflectionUtils.isSystemTypeNotArray(type) && !annotatedType.isSchemaProperty() && !annotatedType.isResolveAsRef()) {
// Special handling for java.util.stream.Stream - treat it as an array type
if (!isStreamType(type) && ReflectionUtils.isSystemTypeNotArray(type) && !annotatedType.isSchemaProperty() && !annotatedType.isResolveAsRef()) {
context.resolve(new AnnotatedType().components(annotatedType.getComponents()).type(valueType).jsonViewAnnotation(annotatedType.getJsonViewAnnotation()));
return null;
}
Expand Down Expand Up @@ -1147,11 +1178,17 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context
StringUtils.isNotBlank(model.getName())) {
if (context.getDefinedModels().containsKey(model.getName())) {
if (!Schema.SchemaResolution.INLINE.equals(resolvedSchemaResolution)) {
model = new Schema().$ref(constructRef(model.getName()));
// Preserve allOf and other composition keywords when creating reference
Schema refSchema = new Schema().$ref(constructRef(model.getName()));
preserveCompositionKeywords(model, refSchema);
model = refSchema;
}
}
} else if (model != null && model.get$ref() != null) {
model = new Schema().$ref(StringUtils.isNotEmpty(model.get$ref()) ? model.get$ref() : model.getName());
Schema refSchema = new Schema().$ref(StringUtils.isNotEmpty(model.get$ref()) ? model.get$ref() : model.getName());
// Preserve allOf and other composition keywords when creating reference
preserveCompositionKeywords(model, refSchema);
model = refSchema;
}

if (model != null && resolvedArrayAnnotation != null) {
Expand Down Expand Up @@ -3657,4 +3694,31 @@ private Optional<Schema> resolveArraySchemaWithCycleGuard(
}
return reResolvedProperty;
}

/**
* Checks if the given JavaType represents java.util.stream.Stream.
*/
private boolean isStreamType(JavaType type) {
try {
return java.util.stream.Stream.class.isAssignableFrom(type.getRawClass());
} catch (Exception e) {
// Fallback to string comparison if class loading fails
return type.getRawClass().getName().equals("java.util.stream.Stream");
}
}

/**
* Preserves composition keywords (allOf, anyOf, oneOf) from source to target schema.
*/
private void preserveCompositionKeywords(Schema source, Schema target) {
if (source.getAllOf() != null && !source.getAllOf().isEmpty()) {
target.setAllOf(source.getAllOf());
}
if (source.getAnyOf() != null && !source.getAnyOf().isEmpty()) {
target.setAnyOf(source.getAnyOf());
}
if (source.getOneOf() != null && !source.getOneOf().isEmpty()) {
target.setOneOf(source.getOneOf());
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.swagger.v3.core.converting;

import io.swagger.v3.core.converter.AnnotatedType;
import io.swagger.v3.oas.models.media.Schema;
import org.testng.annotations.Test;

import java.lang.annotation.Annotation;
Expand Down Expand Up @@ -148,4 +149,34 @@ public void testEquals_shouldBeEqualWhenSchemaPropertyIsTrueAndNamesMatch() {
assertEquals(complexPropA.hashCode(), complexPropC.hashCode(),
"When schemaProperty is true, hash codes must be equal if propertyNames are the same.");
}

/**
* Documents issue #5043 - parent field in equals/hashCode would prevent
* cache hits from propagating annotations, but including it breaks ArrayOfSubclassTest.
* A more targeted solution is needed.
*/
@Test(enabled = false)
public void testEquals_parentFieldIssue() {
Schema<?> parentSchema1 = new Schema<>();
parentSchema1.setDeprecated(true);

Schema<?> parentSchema2 = new Schema<>();
parentSchema2.setDeprecated(false);

Annotation annA = getAnnotationInstance(TestAnnA.class);
Annotation[] annotations = {annA};

AnnotatedType type1 = new AnnotatedType(String.class)
.ctxAnnotations(annotations)
.parent(parentSchema1);

AnnotatedType type2 = new AnnotatedType(String.class)
.ctxAnnotations(annotations)
.parent(parentSchema2);

// Currently these are equal (parent not in equals/hashCode)
// Issue #5043 suggests they should not be equal to prevent annotation leakage
// However, including parent breaks ArrayOfSubclassTest
assertEquals(type1, type2);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package io.swagger.v3.core.resolving;

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.core.resolving.resources.StreamModel;
import io.swagger.v3.oas.models.media.Schema;
import org.testng.annotations.Test;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;

/**
* Regression test for issue #5013 - Stream ArraySchema Wrong Type in OAS 3.1
*/
public class Ticket5013StreamArraySchemaTest extends SwaggerTestBase {

@Test
public void testStreamPropertyGeneratesArraySchemaInOAS31() throws Exception {
final ModelResolver modelResolver = new ModelResolver(mapper()).openapi31(true);
final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver);

final Schema model = context.resolve(new AnnotatedType(StreamModel.class));
assertNotNull(model);

Schema greetingsProperty = (Schema) model.getProperties().get("greetings");
assertNotNull(greetingsProperty, "greetings property should exist");

// In OAS 3.1, use types array instead of type
assertNotNull(greetingsProperty.getTypes(), "types should not be null");
assertEquals(greetingsProperty.getTypes().size(), 1, "Should have exactly one type");
assertTrue(greetingsProperty.getTypes().contains("array"),
"Stream<Greeting> should generate types containing 'array' in OAS 3.1");

assertNotNull(greetingsProperty.getItems(),
"Array schema should have items defined");
}

@Test
public void testStreamPropertyGeneratesArraySchemaInOAS30() throws Exception {
final ModelResolver modelResolver = new ModelResolver(mapper());
final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver);

final Schema model = context.resolve(new AnnotatedType(StreamModel.class));
assertNotNull(model);

Schema greetingsProperty = (Schema) model.getProperties().get("greetings");
assertNotNull(greetingsProperty, "greetings property should exist");

// In OAS 3.0, should be array type
assertEquals(greetingsProperty.getType(), "array",
"Stream<Greeting> should generate type: array in OAS 3.0");

assertNotNull(greetingsProperty.getItems(),
"Array schema should have items defined");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package io.swagger.v3.core.resolving;

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.core.matchers.SerializationMatchers;
import io.swagger.v3.core.resolving.resources.AttributeType;
import io.swagger.v3.core.resolving.resources.DateAttributeTypeImpl;
import io.swagger.v3.oas.models.media.Schema;
import org.testng.annotations.Test;

import static org.testng.Assert.assertNotNull;

/**
* Regression test for issue #5028 - Polymorphic Types Missing allOf
*/
public class Ticket5028PolymorphicAllOfTest extends SwaggerTestBase {

@Test
public void testSubtypePreservesAllOf() throws Exception {
final ModelResolver modelResolver = new ModelResolver(mapper());
final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver);

// Resolve the parent interface
final Schema parentModel = context.resolve(new AnnotatedType(AttributeType.class));
assertNotNull(parentModel);

// Resolve the subtype
final Schema subtypeModel = context.resolve(new AnnotatedType(DateAttributeTypeImpl.class));
assertNotNull(subtypeModel);

// Verify that DateAttributeTypeImpl has allOf reference to AttributeType
SerializationMatchers.assertEqualsToYaml(context.getDefinedModels(),
"AttributeType:\n" +
" required:\n" +
" - type\n" +
" type: object\n" +
" properties:\n" +
" name:\n" +
" type: string\n" +
" type:\n" +
" type: string\n" +
" discriminator:\n" +
" propertyName: type\n" +
"DateAttributeTypeImpl:\n" +
" type: object\n" +
" allOf:\n" +
" - $ref: \"#/components/schemas/AttributeType\"\n" +
" - type: object\n" +
" properties:\n" +
" format:\n" +
" type: string\n");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.swagger.v3.core.resolving.resources;

import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = DateAttributeTypeImpl.class, name = "date")
})
public interface AttributeType {
String getName();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.swagger.v3.core.resolving.resources;

public class DateAttributeTypeImpl implements AttributeType {
private String name;
private String format;

@Override
public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getFormat() {
return format;
}

public void setFormat(String format) {
this.format = format;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.swagger.v3.core.resolving.resources;

import io.swagger.v3.oas.annotations.media.ArraySchema;

public class StreamModel {

public static class Greeting {
private String message;

public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}
}

@ArraySchema
private java.util.stream.Stream<Greeting> greetings;

@ArraySchema
public java.util.stream.Stream<Greeting> getGreetings() {
return greetings;
}

public void setGreetings(java.util.stream.Stream<Greeting> greetings) {
this.greetings = greetings;
}
}