From 6d0bcb5d04ac0f8bea415f61e9f963da43159956 Mon Sep 17 00:00:00 2001 From: Mattias-Sehlstedt <60173714+Mattias-Sehlstedt@users.noreply.github.com> Date: Thu, 3 Sep 2026 21:34:02 +0200 Subject: [PATCH] fix: ignore a nullable annotation for enums that use enumAsRef --- .../v3/core/jackson/ModelResolver.java | 54 +++++++------ .../v3/core/converting/EnumPropertyTest.java | 76 +++++++++++++++++-- .../oas/models/ModelWithEnumRefProperty.java | 2 +- .../models/ModelWithNullableEnumProperty.java | 16 ++++ .../ModelWithNullableEnumRefProperty.java | 16 ++++ 5 files changed, 132 insertions(+), 32 deletions(-) create mode 100644 modules/swagger-core/src/test/java/io/swagger/v3/core/oas/models/ModelWithNullableEnumProperty.java create mode 100644 modules/swagger-core/src/test/java/io/swagger/v3/core/oas/models/ModelWithNullableEnumRefProperty.java 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 b281d34855..57d9be4af6 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 @@ -129,6 +129,7 @@ public class ModelResolver extends AbstractModelConverter implements ModelConver private static final int SCHEMA_COMPONENT_PREFIX = "#/components/schemas/".length(); private static final String OBJECT_TYPE = "object"; + private static final String NULL_TYPE = "null"; private static final Predicate ANNOTATIONS_THAT_SHOULD_BE_STRIPPED_FOR_CONTAINER_ITEMS = annotation -> annotation.annotationType().getName().startsWith("io.swagger") || @@ -235,7 +236,7 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context schema.specVersion(SpecVersion.V31); } resolveArraySchema(annotatedType, schema, resolvedArrayAnnotation); - Schema itemsSchema = openapi31 ? new JsonSchema() : new Schema(); + Schema itemsSchema = getNewSchema(); return schema.items(itemsSchema.$ref(resolvedSchemaAnnotation.ref()).name(name)); } } @@ -286,12 +287,12 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context // create a reference for the items if (context.getDefinedModels().containsKey(innerSchema.getName())) { String ref = constructRef(innerSchema.getName()); - innerSchema = openapi31 ? new JsonSchema() : new Schema(); + innerSchema = getNewSchema(); innerSchema.$ref(ref); } } else if (innerSchema != null && innerSchema.get$ref() != null) { String ref = StringUtils.isNotEmpty(innerSchema.get$ref()) ? innerSchema.get$ref() : innerSchema.getName(); - innerSchema = openapi31 ? new JsonSchema() : new Schema(); + innerSchema = getNewSchema(); innerSchema.$ref(ref); } } @@ -303,12 +304,12 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context // create a reference for the items if (context.getDefinedModels().containsKey(implSchema.getName())) { String ref = constructRef(implSchema.getName()); - implSchema = openapi31 ? new JsonSchema() : new Schema(); + implSchema = getNewSchema(); implSchema.$ref(ref); } } else if (implSchema != null && implSchema.get$ref() != null) { String ref = StringUtils.isNotEmpty(implSchema.get$ref()) ? implSchema.get$ref() : implSchema.getName(); - implSchema = openapi31 ? new JsonSchema() : new Schema(); + implSchema = getNewSchema(); implSchema.$ref(ref); } return implSchema; @@ -382,7 +383,7 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context } if ("Object".equals(name)) { - Schema schema = openapi31 ? new JsonSchema() : new Schema(); + Schema schema = getNewSchema(); if (schemaRefFromAnnotation != null) { schema.raw$ref(schemaRefFromAnnotation); } @@ -414,7 +415,7 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context // 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 = getNewSchema(); model.$ref(Components.COMPONENTS_SCHEMAS_REF + name); } if (!hasCompositionKeywords) { @@ -510,13 +511,13 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context addPropertiesSchema = context.getDefinedModels().get(pName); } else { // create a reference for the items - addPropertiesSchema = openapi31 ? new JsonSchema() : new Schema(); + addPropertiesSchema = getNewSchema(); addPropertiesSchema.$ref(constructRef(pName)); } } } else if (addPropertiesSchema.get$ref() != null) { String ref = StringUtils.isNotEmpty(addPropertiesSchema.get$ref()) ? addPropertiesSchema.get$ref() : addPropertiesSchema.getName(); - addPropertiesSchema = openapi31 ? new JsonSchema() : new Schema(); + addPropertiesSchema = getNewSchema(); addPropertiesSchema.$ref(ref); } } @@ -569,13 +570,13 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context items = context.getDefinedModels().get(pName); } else { // create a reference for the items - items = openapi31 ? new JsonSchema() : new Schema(); + items = getNewSchema(); items.$ref(constructRef(pName)); } } } else if (items.get$ref() != null) { String ref = StringUtils.isNotEmpty(items.get$ref()) ? items.get$ref() : items.getName(); - items = openapi31 ? new JsonSchema() : new Schema(); + items = getNewSchema(); items.$ref(ref); } @@ -803,7 +804,7 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context handleUnwrapped(props, innerModel, uw.prefix(), uw.suffix(), requiredProps); return null; } else { - return openapi31 ? new JsonSchema() : new Schema(); + return getNewSchema(); } }); @@ -875,14 +876,14 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context if (Schema.SchemaResolution.INLINE.equals(resolvedSchemaResolution)) { property = context.getDefinedModels().get(pName); } else if (Schema.SchemaResolution.ALL_OF.equals(resolvedSchemaResolution) && ctxProperty != null) { - property = openapi31 ? new JsonSchema() : new Schema(); + property = getNewSchema(); property .addAllOfItem(ctxProperty) .addAllOfItem(openapi31 ? new JsonSchema().$ref(constructRef(pName)) : new Schema().$ref(constructRef(pName))); } else if (Schema.SchemaResolution.ALL_OF_REF.equals(resolvedSchemaResolution) && ctxProperty != null) { property = ctxProperty.addAllOfItem(openapi31 ? new JsonSchema().$ref(constructRef(pName)) : new Schema().$ref(constructRef(pName))); } else { - property = openapi31 ? new JsonSchema() : new Schema(); + property = getNewSchema(); property.$ref(constructRef(pName)); } property = clone(property); @@ -1701,7 +1702,7 @@ protected Schema process(Schema id, String propertyName, AnnotatedType type, model = resolve(type, context, null); } model.addProperties(propertyName, id); - Schema retSchema = openapi31 ? new JsonSchema() : new Schema(); + Schema retSchema = getNewSchema(); return retSchema.$ref(StringUtils.isNotEmpty(model.get$ref()) ? model.get$ref() : model.getName()); } @@ -2084,7 +2085,7 @@ private boolean resolveSubtypes(Schema model, BeanDescription bean, ModelConvert } else { composedSchema = (ComposedSchema) subtypeModel; } - Schema refSchema = openapi31 ? new JsonSchema() : new Schema(); + Schema refSchema = getNewSchema(); refSchema.$ref(Components.COMPONENTS_SCHEMAS_REF + model.getName()); // allOf could have already being added during type resolving when @Schema(allOf..) is declared if (composedSchema.getAllOf() == null || !composedSchema.getAllOf().contains(refSchema)) { @@ -2981,7 +2982,7 @@ protected String resolveContentMediaType(Annotated a, Annotation[] annotations, protected void resolveContains(AnnotatedType annotatedType, ArraySchema arraySchema, io.swagger.v3.oas.annotations.media.ArraySchema arraySchemaAnnotation) { final io.swagger.v3.oas.annotations.media.Schema containsAnnotation = arraySchemaAnnotation.contains(); - final Schema contains = openapi31 ? new JsonSchema() : new Schema(); + final Schema contains = getNewSchema(); if (containsAnnotation.types().length > 0) { for (String type : containsAnnotation.types()) { contains.addType(type); @@ -3002,7 +3003,7 @@ protected void resolveContains(AnnotatedType annotatedType, ArraySchema arraySch protected void resolveUnevaluatedItems(AnnotatedType annotatedType, ArraySchema arraySchema, io.swagger.v3.oas.annotations.media.ArraySchema arraySchemaAnnotation) { final io.swagger.v3.oas.annotations.media.Schema unevaluatedItemsAnnotation = arraySchemaAnnotation.unevaluatedItems(); - final Schema unevaluatedItems = openapi31 ? new JsonSchema() : new Schema(); + final Schema unevaluatedItems = getNewSchema(); if (StringUtils.isNotBlank(unevaluatedItemsAnnotation.type())) { unevaluatedItems.addType(unevaluatedItemsAnnotation.type()); } @@ -3209,14 +3210,14 @@ protected void resolveSchemaMembers(Schema schema, Annotated a, Annotation[] ann Object defaultValue = resolveDefaultValue(a, annotations, schemaAnnotation); if (defaultValue != null) { schema.setDefault(defaultValue); - } else if (schemaAnnotation != null && "null".equals(schemaAnnotation.defaultValue().trim()) && schemaAnnotation.nullable()) { + } else if (schemaAnnotation != null && NULL_TYPE.equals(schemaAnnotation.defaultValue().trim()) && schemaAnnotation.nullable()) { // Explicitly set to null when defaultValue="null" AND nullable=true schema.setDefault(null); } Object example = resolveExample(a, annotations, schemaAnnotation); if (example != null) { schema.example(example); - } else if (schemaAnnotation != null && "null".equals(schemaAnnotation.example().trim()) && schemaAnnotation.nullable()) { + } else if (schemaAnnotation != null && NULL_TYPE.equals(schemaAnnotation.example().trim()) && schemaAnnotation.nullable()) { // Explicitly set to null when example="null" AND nullable=true schema.example(null); } @@ -3225,10 +3226,13 @@ protected void resolveSchemaMembers(Schema schema, Annotated a, Annotation[] ann schema.readOnly(readOnly); } Boolean nullable = resolveNullable(a, annotations, schemaAnnotation); - if (nullable != null) { + boolean isEnum = a != null && a.getRawType().isEnum(); + boolean enumAsRefAnnotation = schemaAnnotation != null && schemaAnnotation.enumAsRef(); + boolean isEnumThatWillBecomeRef = isEnum && (enumAsRefAnnotation || ModelResolver.enumsAsRef); + if (nullable != null && !isEnumThatWillBecomeRef) { schema.nullable(nullable); if (openapi31 && nullable) { - schema.addType("null"); + schema.addType(NULL_TYPE); } } BigDecimal multipleOf = resolveMultipleOf(a, annotations, schemaAnnotation); @@ -3613,7 +3617,7 @@ protected Schema buildRefSchemaIfObject(Schema schema, ModelConverterContext con Schema result = schema; if (isObjectSchema(schema) && StringUtils.isNotBlank(schema.getName())) { if (context.getDefinedModels().containsKey(schema.getName())) { - result = openapi31 ? new JsonSchema() : new Schema(); + result = getNewSchema(); result.$ref(constructRef(schema.getName())); } } @@ -3677,6 +3681,10 @@ private io.swagger.v3.oas.annotations.media.ArraySchema getArraySchemaAnnotation } } + private Schema getNewSchema() { + return openapi31 ? new JsonSchema() : new Schema(); + } + /** * Checks if the given JavaType represents a java.util.stream.Stream */ diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/converting/EnumPropertyTest.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/converting/EnumPropertyTest.java index 915710650a..de25108924 100644 --- a/modules/swagger-core/src/test/java/io/swagger/v3/core/converting/EnumPropertyTest.java +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/converting/EnumPropertyTest.java @@ -7,14 +7,7 @@ import io.swagger.v3.core.jackson.ModelResolver; import io.swagger.v3.core.jackson.TypeNameResolver; import io.swagger.v3.core.matchers.SerializationMatchers; -import io.swagger.v3.core.oas.models.JacksonValueBridgeMethodEnum; -import io.swagger.v3.core.oas.models.JacksonValueDefaultMethodEnum; -import io.swagger.v3.core.oas.models.JacksonValuePrivateEnum; -import io.swagger.v3.core.oas.models.Model1979; -import io.swagger.v3.core.oas.models.ModelWithEnumField; -import io.swagger.v3.core.oas.models.ModelWithEnumProperty; -import io.swagger.v3.core.oas.models.ModelWithEnumRefProperty; -import io.swagger.v3.core.oas.models.ModelWithJacksonEnumField; +import io.swagger.v3.core.oas.models.*; import io.swagger.v3.oas.models.media.IntegerSchema; import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.oas.models.media.StringSchema; @@ -170,6 +163,52 @@ public void testEnumRefPropertyWithFQNTypeNameResolver() { } + @Test(description = "it should read a model with an enum property as a reference") + public void testEnumNullableRefProperty() { + context.resolve(new AnnotatedType(ModelWithNullableEnumRefProperty.class)); + final Map models = context.getDefinedModels(); + final String yaml = "ModelWithNullableEnumRefProperty:\n" + + " type: object\n" + + " properties:\n" + + " enumAsRef:\n" + + " $ref: \"#/components/schemas/TestSecondEnum\"\n" + + " nullableEnumAsRef:\n" + + " $ref: \"#/components/schemas/TestSecondEnum\"\n" + + "TestSecondEnum:\n" + + " type: string\n" + + " enum:\n" + + " - A_PRIVATE\n" + + " - A_PUBLIC\n" + + " - A_SYSTEM\n" + + " - A_INVITE_ONLY\n"; + SerializationMatchers.assertEqualsToYaml(models, yaml); + + } + + @Test(description = "it should read a model with an enum property as a reference with fqn TypeNameResolver") + public void testEnumNullableRefPropertyWithFQNTypeNameResolver() { + TypeNameResolver.std.setUseFqn(true); + context.resolve(new AnnotatedType(ModelWithNullableEnumRefProperty.class)); + final Map models = context.getDefinedModels(); + final String yaml = "io.swagger.v3.core.oas.models.ModelWithNullableEnumRefProperty:\n" + + " type: object\n" + + " properties:\n" + + " enumAsRef:\n" + + " $ref: \"#/components/schemas/io.swagger.v3.core.oas.models.TestSecondEnum\"\n" + + " nullableEnumAsRef:\n" + + " $ref: \"#/components/schemas/io.swagger.v3.core.oas.models.TestSecondEnum\"\n" + + "io.swagger.v3.core.oas.models.TestSecondEnum:\n" + + " type: string\n" + + " enum:\n" + + " - A_PRIVATE\n" + + " - A_PUBLIC\n" + + " - A_SYSTEM\n" + + " - A_INVITE_ONLY\n"; + TypeNameResolver.std.setUseFqn(false); + SerializationMatchers.assertEqualsToYaml(models, yaml); + + } + @Test(description = "it should read a model with an enum property as a reference, set via static var or sys prop") public void testEnumRefPropertyGlobal() { ModelResolver.enumsAsRef = true; @@ -191,6 +230,27 @@ public void testEnumRefPropertyGlobal() { ModelResolver.enumsAsRef = false; } + @Test + public void testNullableEnumRefPropertyGlobal() { + ModelResolver.enumsAsRef = true; + context.resolve(new AnnotatedType(ModelWithNullableEnumProperty.class)); + final Map models = context.getDefinedModels(); + final String yaml = "ModelWithNullableEnumProperty:\n" + + " type: object\n" + + " properties:\n" + + " enumValue:\n" + + " $ref: \"#/components/schemas/TestEnum\"\n" + + "TestEnum:\n" + + " type: string\n" + + " enum:\n" + + " - PRIVATE\n" + + " - PUBLIC\n" + + " - SYSTEM\n" + + " - INVITE_ONLY\n"; + SerializationMatchers.assertEqualsToYaml(models, yaml); + ModelResolver.enumsAsRef = false; + } + @Test(description = "it should not affect non-enum models when the enumsAsRef property is enabled globally") public void testEnumRefPropertyGlobalNotAffectingNonEnums() { ModelResolver.enumsAsRef = true; diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/oas/models/ModelWithEnumRefProperty.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/oas/models/ModelWithEnumRefProperty.java index 3fd6235c0f..240de13981 100644 --- a/modules/swagger-core/src/test/java/io/swagger/v3/core/oas/models/ModelWithEnumRefProperty.java +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/oas/models/ModelWithEnumRefProperty.java @@ -17,7 +17,7 @@ public TestEnum getA() { } public void setA(TestEnum e) { - this.a = a; + this.a = e; } @Schema(enumAsRef = true) diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/oas/models/ModelWithNullableEnumProperty.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/oas/models/ModelWithNullableEnumProperty.java new file mode 100644 index 0000000000..22bcf14649 --- /dev/null +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/oas/models/ModelWithNullableEnumProperty.java @@ -0,0 +1,16 @@ +package io.swagger.v3.core.oas.models; + +import javax.annotation.Nullable; + +public class ModelWithNullableEnumProperty { + private TestEnum e; + + @Nullable + public TestEnum getEnumValue() { + return e; + } + + public void setEnumValue(TestEnum e) { + this.e = e; + } +} diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/oas/models/ModelWithNullableEnumRefProperty.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/oas/models/ModelWithNullableEnumRefProperty.java new file mode 100644 index 0000000000..9124c0e9af --- /dev/null +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/oas/models/ModelWithNullableEnumRefProperty.java @@ -0,0 +1,16 @@ +package io.swagger.v3.core.oas.models; + +import io.swagger.v3.oas.annotations.media.Schema; + +import javax.annotation.Nullable; + +public class ModelWithNullableEnumRefProperty { + + @Schema(enumAsRef = true) + public TestSecondEnum enumAsRef; + + @Nullable + @Schema(enumAsRef = true) + public TestSecondEnum nullableEnumAsRef; + +}