Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -215,10 +215,25 @@
/**
* If true, designates a value as possibly null.
*
* @deprecated As of 2.2.51, replaced by {@link #nullableMode()}
*
* @return whether or not this schema is nullable
**/
@Deprecated
boolean nullable() default false;

/**
* Allows to specify the nullable mode (NullableMode.AUTO, NULLABLE, NOT_NULLABLE)
*
* NullableMode.AUTO: will let the library decide based on its heuristics (e.g. @Nullable annotation auto-detection).
* NullableMode.NULLABLE: will force the item to be considered as nullable regardless of heuristics.
* NullableMode.NOT_NULLABLE: will force the item to be considered as not nullable regardless of heuristics.
*
* @return the nullableMode for this schema (property)
*
*/
NullableMode nullableMode() default NullableMode.AUTO;

/**
* Sets whether the value should only be read during a response but not read to during a request.
*
Expand Down Expand Up @@ -567,6 +582,12 @@ enum RequiredMode {
NOT_REQUIRED;
}

enum NullableMode {
AUTO,
NULLABLE,
NOT_NULLABLE;
}

enum SchemaResolution {
AUTO,
DEFAULT,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2333,12 +2333,12 @@ protected Object resolveDefaultValue(Annotated a, Annotation[] annotations, io.s
try {
ObjectMapper mapper = ObjectMapperFactory.buildStrictGenericObjectMapper();
JsonNode node = mapper.readTree(schema.defaultValue());
// Only return null for "null" string when nullable=true
// Only return null for "null" string when the schema is effectively nullable
if (node.isNull()) {
if (schema.nullable()) {
if (isSchemaAnnotationNullable(schema)) {
return null;
} else {
// When nullable=false, treat "null" as literal string
// When not nullable, treat "null" as literal string
return schema.defaultValue();
}
}
Expand Down Expand Up @@ -2377,7 +2377,7 @@ protected Object resolveExample(Annotated a, Annotation[] annotations, io.swagge
ObjectMapper mapper = ObjectMapperFactory.buildStrictGenericObjectMapper();
JsonNode node = mapper.readTree(schema.example());
if (node.isNull()) {
if (schema.nullable()) {
if (isSchemaAnnotationNullable(schema)) {
return null;
} else {
return schema.example();
Expand Down Expand Up @@ -2496,8 +2496,27 @@ protected Boolean resolveReadOnly(Annotated a, Annotation[] annotations, io.swag
}

protected Boolean resolveNullable(Annotated a, Annotation[] annotations, io.swagger.v3.oas.annotations.media.Schema schema) {
if (schema != null && schema.nullable()) {
return true;
// NullableMode on the explicit @Schema parameter takes precedence over both the legacy
// nullable boolean and auto-detection. NullableMode handling for property-level @Schema
// (when this method is called with schema=null) is centralized in AnnotationsUtils, where
// the @Schema annotation is the canonical source - per the project convention that
// @Schema must not be scanned out of the annotations array (sibling handling depends on
// that separation).
if (schema != null) {
io.swagger.v3.oas.annotations.media.Schema.NullableMode mode = schema.nullableMode();
if (mode == io.swagger.v3.oas.annotations.media.Schema.NullableMode.NULLABLE) {
return true;
}
if (mode == io.swagger.v3.oas.annotations.media.Schema.NullableMode.NOT_NULLABLE) {
// Returning null (not Boolean.FALSE) avoids the caller emitting `"nullable": false`
// literally; AnnotationsUtils clears any prior nullable indication when it processes
// the @Schema annotation.
return null;
}
// mode == AUTO: honor legacy nullable boolean if set, otherwise fall through to heuristics.
if (schema.nullable()) {
return true;
}
}
if (annotations != null) {
for (Annotation annotation : annotations) {
Expand All @@ -2509,6 +2528,30 @@ protected Boolean resolveNullable(Annotated a, Annotation[] annotations, io.swag
return null;
}

/**
* Returns true if the @Schema annotation effectively declares nullable, honoring
* {@code nullableMode} with precedence over the legacy {@code nullable} boolean.
* Returns false if the annotation explicitly declares not-nullable (NullableMode.NOT_NULLABLE)
* or if no nullability intent is expressed.
*
* <p>Used for derivative decisions (e.g. whether a "null" defaultValue/example
* literal should be treated as actual null). Does not consult auto-detected
* @Nullable annotations - for full resolution use {@link #resolveNullable}.
*/
private static boolean isSchemaAnnotationNullable(io.swagger.v3.oas.annotations.media.Schema schema) {
if (schema == null) {
return false;
}
io.swagger.v3.oas.annotations.media.Schema.NullableMode mode = schema.nullableMode();
if (mode == io.swagger.v3.oas.annotations.media.Schema.NullableMode.NULLABLE) {
return true;
}
if (mode == io.swagger.v3.oas.annotations.media.Schema.NullableMode.NOT_NULLABLE) {
return false;
}
return schema.nullable();
}

protected BigDecimal resolveMultipleOf(Annotated a, Annotation[] annotations, io.swagger.v3.oas.annotations.media.Schema schema) {
if (schema != null && schema.multipleOf() != 0) {
return new BigDecimal(schema.multipleOf());
Expand Down Expand Up @@ -3187,15 +3230,15 @@ 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()) {
// Explicitly set to null when defaultValue="null" AND nullable=true
} else if (schemaAnnotation != null && "null".equals(schemaAnnotation.defaultValue().trim()) && isSchemaAnnotationNullable(schemaAnnotation)) {
// Explicitly set to null when defaultValue="null" AND the schema is effectively nullable
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()) {
// Explicitly set to null when example="null" AND nullable=true
} else if (schemaAnnotation != null && "null".equals(schemaAnnotation.example().trim()) && isSchemaAnnotationNullable(schemaAnnotation)) {
// Explicitly set to null when example="null" AND the schema is effectively nullable
schema.example(null);
}
Boolean readOnly = resolveReadOnly(a, annotations, schemaAnnotation);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ public static boolean hasSchemaAnnotation(io.swagger.v3.oas.annotations.media.Sc
&& !schema.required()
&& schema.requiredMode().equals(io.swagger.v3.oas.annotations.media.Schema.RequiredMode.AUTO)
&& !schema.nullable()
&& schema.nullableMode().equals(io.swagger.v3.oas.annotations.media.Schema.NullableMode.AUTO)
&& !schema.readOnly()
&& !schema.writeOnly()
&& schema.accessMode().equals(io.swagger.v3.oas.annotations.media.Schema.AccessMode.AUTO)
Expand Down Expand Up @@ -293,6 +294,9 @@ public static boolean equals(io.swagger.v3.oas.annotations.media.Schema thisSche
if (thisSchema.nullable() != thatSchema.nullable()) {
return false;
}
if (!thisSchema.nullableMode().equals(thatSchema.nullableMode())) {
return false;
}
if (thisSchema.readOnly() != thatSchema.readOnly()) {
return false;
}
Expand Down Expand Up @@ -835,12 +839,28 @@ public static Optional<Schema> getSchemaFromAnnotation(
String filteredMinimum = schema.minimum().replace(Constants.COMMA, StringUtils.EMPTY);
schemaObject.setMinimum(new BigDecimal(filteredMinimum));
}
if (schema.nullable()) {
// Honor nullableMode (NULLABLE/NOT_NULLABLE) with precedence over legacy nullable boolean.
io.swagger.v3.oas.annotations.media.Schema.NullableMode nullableMode = schema.nullableMode();
boolean effectiveNullable;
if (nullableMode == io.swagger.v3.oas.annotations.media.Schema.NullableMode.NULLABLE) {
effectiveNullable = true;
} else if (nullableMode == io.swagger.v3.oas.annotations.media.Schema.NullableMode.NOT_NULLABLE) {
effectiveNullable = false;
} else {
effectiveNullable = schema.nullable();
}
if (effectiveNullable) {
if (openapi31) {
schemaObject.addType("null");
} else {
schemaObject.setNullable(true);
}
} else if (nullableMode == io.swagger.v3.oas.annotations.media.Schema.NullableMode.NOT_NULLABLE) {
// Explicit NOT_NULLABLE overrides prior nullable indications (e.g. from @Nullable auto-detection).
if (openapi31 && schemaObject.getTypes() != null) {
schemaObject.getTypes().remove("null");
}
schemaObject.setNullable(null);
}
if (StringUtils.isNotBlank(schema.title())) {
schemaObject.setTitle(schema.title());
Expand Down Expand Up @@ -2410,6 +2430,14 @@ public RequiredMode requiredMode() {
return patch.requiredMode();
}

@Override
public NullableMode nullableMode() {
if (!master.nullableMode().equals(NullableMode.AUTO) || patch.nullableMode().equals(NullableMode.AUTO)) {
return master.nullableMode();
}
return patch.nullableMode();
}

@Override
public String description() {
if (StringUtils.isNotBlank(master.description()) || StringUtils.isBlank(patch.description())) {
Expand Down
Loading