ComplexPolymorphicModelTest.java
package io.swagger.v3.core.converting;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.core.converter.AnnotatedType;
import io.swagger.v3.core.converter.ModelConverterContextImpl;
import io.swagger.v3.core.converter.ModelConverters;
import io.swagger.v3.core.converter.ResolvedSchema;
import io.swagger.v3.core.jackson.ModelResolver;
import io.swagger.v3.core.oas.models.ModelWithManySubtypesAndRecursion;
import io.swagger.v3.oas.models.media.ArraySchema;
import io.swagger.v3.oas.models.media.Schema;
import org.testng.annotations.Test;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.Set;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;
public class ComplexPolymorphicModelTest {
@Test
@SuppressWarnings("unchecked")
public void complexPolymorphicModelNoDuplicateResolutionTest() throws Exception {
ModelConverterContextImpl context = new ModelConverterContextImpl(new ModelResolver(new ObjectMapper()));
context.resolve(new AnnotatedType(ModelWithManySubtypesAndRecursion.Holder.class));
Field processedTypesField = ModelConverterContextImpl.class.getDeclaredField("processedTypes");
processedTypesField.setAccessible(true);
Set<AnnotatedType> processedTypes = (Set<AnnotatedType>) processedTypesField.get(context);
Class<?>[] subtypes = {
ModelWithManySubtypesAndRecursion.Base.class,
ModelWithManySubtypesAndRecursion.SubA.class,
ModelWithManySubtypesAndRecursion.SubB.class,
ModelWithManySubtypesAndRecursion.SubC.class,
ModelWithManySubtypesAndRecursion.SubD.class,
ModelWithManySubtypesAndRecursion.SubE.class,
ModelWithManySubtypesAndRecursion.SubF.class,
ModelWithManySubtypesAndRecursion.SubG.class,
ModelWithManySubtypesAndRecursion.SubH.class,
ModelWithManySubtypesAndRecursion.SubI.class,
ModelWithManySubtypesAndRecursion.SubJ.class,
};
for (Class<?> subtype : subtypes) {
long count = processedTypes.stream()
.filter(at -> subtype.equals(at.getType()))
.count();
assertEquals(count, 1,
subtype.getSimpleName() + " should be cached and resolved only once, not " + count + " times");
}
}
@Test
public void testSchemaStructureCorrectness() {
ResolvedSchema resolvedSchema = ModelConverters.getInstance(false)
.readAllAsResolvedSchema(ModelWithManySubtypesAndRecursion.Holder.class);
assertNotNull(resolvedSchema, "ResolvedSchema should not be null");
assertNotNull(resolvedSchema.referencedSchemas, "Referenced schemas should not be null");
Schema baseSchema = resolvedSchema.referencedSchemas.get("Base");
assertNotNull(baseSchema, "Base schema should be in referenced schemas");
assertNotNull(baseSchema.getDiscriminator(), "Base schema should have discriminator");
assertEquals(baseSchema.getDiscriminator().getPropertyName(), "name",
"Discriminator property should be 'name'");
Map<String, String> mappings = baseSchema.getDiscriminator().getMapping();
assertNotNull(mappings, "Discriminator mappings should not be null");
assertEquals(mappings.size(), 10, "Should have 10 discriminator mappings");
String[] expectedMappings = {"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"};
String[] expectedSchemas = {"SubA", "SubB", "SubC", "SubD", "SubE", "SubF", "SubG", "SubH", "SubI", "SubJ"};
for (int i = 0; i < expectedMappings.length; i++) {
String mapping = expectedMappings[i];
String schemaName = expectedSchemas[i];
assertTrue(mappings.containsKey(mapping),
"Mapping for '" + mapping + "' should exist");
assertEquals(mappings.get(mapping), "#/components/schemas/" + schemaName,
"Mapping for '" + mapping + "' should reference " + schemaName);
Schema subSchema = resolvedSchema.referencedSchemas.get(schemaName);
assertNotNull(subSchema, schemaName + " should be in referenced schemas");
}
}
@Test
@SuppressWarnings("unchecked")
public void testNonArrayRecursiveReferences() throws Exception {
ModelConverterContextImpl context = new ModelConverterContextImpl(new ModelResolver(new ObjectMapper()));
context.resolve(new AnnotatedType(ModelWithManySubtypesAndRecursion.RecursiveNode.class));
Field processedTypesField = ModelConverterContextImpl.class.getDeclaredField("processedTypes");
processedTypesField.setAccessible(true);
Set<AnnotatedType> processedTypes = (Set<AnnotatedType>) processedTypesField.get(context);
long count = processedTypes.stream()
.filter(at -> ModelWithManySubtypesAndRecursion.RecursiveNode.class.equals(at.getType()))
.count();
assertEquals(count, 1,
"RecursiveNode should be cached and resolved only once despite direct recursion");
Map<String, Schema> definedModels = context.getDefinedModels();
Schema recursiveNodeSchema = definedModels.get("RecursiveNode");
assertNotNull(recursiveNodeSchema, "RecursiveNode schema should exist");
assertNotNull(recursiveNodeSchema.getProperties(), "RecursiveNode should have properties");
Object parentPropObj = recursiveNodeSchema.getProperties().get("parent");
assertNotNull(parentPropObj, "parent property should exist");
assertTrue(parentPropObj instanceof Schema, "parent property should be a Schema");
Schema parentProp = (Schema) parentPropObj;
assertEquals(parentProp.get$ref(), "#/components/schemas/RecursiveNode",
"parent property should reference RecursiveNode");
Object childrenPropObj = recursiveNodeSchema.getProperties().get("children");
assertNotNull(childrenPropObj, "children property should exist");
assertTrue(childrenPropObj instanceof ArraySchema, "children property should be an ArraySchema");
ArraySchema childrenProp = (ArraySchema) childrenPropObj;
Schema items = childrenProp.getItems();
assertNotNull(items, "children items should not be null");
assertEquals(items.get$ref(), "#/components/schemas/RecursiveNode",
"children items should reference RecursiveNode");
}
@Test
@SuppressWarnings("unchecked")
public void testOpenAPI31ResolveSiblingsPath() throws Exception {
ModelResolver resolver = new ModelResolver(new ObjectMapper()).openapi31(true);
ModelConverterContextImpl context = new ModelConverterContextImpl(resolver);
context.resolve(new AnnotatedType(ModelWithManySubtypesAndRecursion.Holder.class));
Field processedTypesField = ModelConverterContextImpl.class.getDeclaredField("processedTypes");
processedTypesField.setAccessible(true);
Set<AnnotatedType> processedTypes = (Set<AnnotatedType>) processedTypesField.get(context);
Class<?>[] subtypes = {
ModelWithManySubtypesAndRecursion.Base.class,
ModelWithManySubtypesAndRecursion.SubA.class,
ModelWithManySubtypesAndRecursion.SubB.class,
ModelWithManySubtypesAndRecursion.SubC.class,
ModelWithManySubtypesAndRecursion.SubD.class,
ModelWithManySubtypesAndRecursion.SubE.class,
ModelWithManySubtypesAndRecursion.SubF.class,
ModelWithManySubtypesAndRecursion.SubG.class,
ModelWithManySubtypesAndRecursion.SubH.class,
ModelWithManySubtypesAndRecursion.SubI.class,
ModelWithManySubtypesAndRecursion.SubJ.class,
};
for (Class<?> subtype : subtypes) {
long count = processedTypes.stream()
.filter(at -> subtype.equals(at.getType()))
.count();
assertEquals(count, 1,
subtype.getSimpleName() + " should be cached in OAS 3.1 mode, not " + count + " times");
}
Map<String, Schema> definedModels = context.getDefinedModels();
Schema baseSchema = definedModels.get("Base");
assertNotNull(baseSchema, "Base schema should exist in OAS 3.1 mode");
}
@Test
public void testDiscriminatorMappingIntegrity() {
ResolvedSchema resolvedSchema = ModelConverters.getInstance(false)
.readAllAsResolvedSchema(ModelWithManySubtypesAndRecursion.Holder.class);
Schema baseSchema = resolvedSchema.referencedSchemas.get("Base");
assertNotNull(baseSchema.getDiscriminator(), "Base should have discriminator");
Map<String, String> mappings = baseSchema.getDiscriminator().getMapping();
for (char c = 'a'; c <= 'j'; c++) {
String key = String.valueOf(c);
assertTrue(mappings.containsKey(key),
"Discriminator should contain mapping for '" + key + "'");
String ref = mappings.get(key);
assertNotNull(ref, "Mapping for '" + key + "' should not be null");
assertTrue(ref.startsWith("#/components/schemas/"),
"Mapping should be a valid $ref: " + ref);
String schemaName = ref.substring("#/components/schemas/".length());
assertNotNull(resolvedSchema.referencedSchemas.get(schemaName),
"Schema '" + schemaName + "' referenced in discriminator mapping should exist");
}
}
@Test
@SuppressWarnings("unchecked")
public void testContextPropagatedThroughArraySchemaAllOf() throws Exception {
ModelResolver resolver = new ModelResolver(new ObjectMapper()).openapi31(true);
ModelConverterContextImpl context = new ModelConverterContextImpl(resolver);
context.resolve(new AnnotatedType(ModelWithManySubtypesAndRecursion.HolderWithAllOfInArraySchema.class));
Field processedTypesField = ModelConverterContextImpl.class.getDeclaredField("processedTypes");
processedTypesField.setAccessible(true);
Set<AnnotatedType> processedTypes = (Set<AnnotatedType>) processedTypesField.get(context);
long count = processedTypes.stream()
.filter(at -> ModelWithManySubtypesAndRecursion.AllOfTarget.class.equals(at.getType()))
.count();
assertEquals(count, 1,
"AllOfTarget referenced via arraySchema.allOf should be resolved through context.resolve() " +
"and appear in processedTypes exactly once, but was found " + count + " times. " +
"This indicates context was not propagated through the 6-arg getSchemaFromAnnotation overload.");
}
@Test
@SuppressWarnings("unchecked")
public void testContextPropagatedThroughItemsSchemaAllOf() throws Exception {
ModelResolver resolver = new ModelResolver(new ObjectMapper()).openapi31(true);
ModelConverterContextImpl context = new ModelConverterContextImpl(resolver);
context.resolve(new AnnotatedType(ModelWithManySubtypesAndRecursion.HolderWithAllOfInItemsSchema.class));
Field processedTypesField = ModelConverterContextImpl.class.getDeclaredField("processedTypes");
processedTypesField.setAccessible(true);
Set<AnnotatedType> processedTypes = (Set<AnnotatedType>) processedTypesField.get(context);
long count = processedTypes.stream()
.filter(at -> ModelWithManySubtypesAndRecursion.AllOfTarget.class.equals(at.getType()))
.count();
assertEquals(count, 1,
"AllOfTarget referenced via @ArraySchema(schema = @Schema(allOf = {...})) should be resolved " +
"through context.resolve() and appear in processedTypes exactly once, but was found " + count + " times.");
}
}