Skip to content
Open
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 @@ -2891,7 +2891,7 @@ protected void updateModelForComposedSchema(CodegenModel m, Schema schema, Map<S
addAdditionPropertiesToCodeGenModel(m, schema);
}

if (Boolean.TRUE.equals(schema.getNullable())) {
if (ModelUtils.isNullable(schema)) {
m.isNullable = Boolean.TRUE;
}

Expand Down Expand Up @@ -3132,7 +3132,7 @@ public CodegenModel fromModel(String name, Schema schema) {
if (!ModelUtils.isArraySchema(schema)) {
m.dataType = getSchemaType(schema);
}
if (!ModelUtils.isAnyType(schema) && Boolean.TRUE.equals(schema.getNullable())) {
if (!ModelUtils.isAnyType(schema) && ModelUtils.isNullable(schema)) {
m.isNullable = Boolean.TRUE;
}

Expand Down Expand Up @@ -4108,9 +4108,7 @@ public CodegenProperty fromProperty(String name, Schema p, boolean required, boo
if (p.getWriteOnly() != null) {
property.isWriteOnly = p.getWriteOnly();
}
if (p.getNullable() != null) {
property.isNullable = p.getNullable();
}
property.isNullable = ModelUtils.isNullable(p);

if (p.getExtensions() != null && !p.getExtensions().isEmpty()) {
property.getVendorExtensions().putAll(p.getExtensions());
Expand Down Expand Up @@ -4160,11 +4158,8 @@ public CodegenProperty fromProperty(String name, Schema p, boolean required, boo
}

// set isNullable using nullable or x-nullable in the schema
if (referencedSchema.getNullable() != null) {
property.isNullable = referencedSchema.getNullable();
} else if (referencedSchema.getExtensions() != null &&
referencedSchema.getExtensions().containsKey(X_NULLABLE)) {
property.isNullable = (Boolean) referencedSchema.getExtensions().get(X_NULLABLE);
if (referencedSchema != p) {
property.isNullable = property.isNullable || ModelUtils.isNullable(referencedSchema);
}

final XML referencedSchemaXml = referencedSchema.getXml();
Expand Down Expand Up @@ -4263,10 +4258,8 @@ public CodegenProperty fromProperty(String name, Schema p, boolean required, boo
if (original != null) {
p = original;
// evaluate common attributes if defined in the top level
if (p.getNullable() != null) {
property.isNullable = p.getNullable();
} else if (p.getExtensions() != null && p.getExtensions().containsKey(X_NULLABLE)) {
property.isNullable = (Boolean) p.getExtensions().get(X_NULLABLE);
if (hasNullableMarker(p)) {
property.isNullable = ModelUtils.isNullable(p);
}

if (p.getReadOnly() != null) {
Expand Down Expand Up @@ -4329,6 +4322,13 @@ public CodegenProperty fromProperty(String name, Schema p, boolean required, boo
* @param cp codegen property
* @param p schema
*/
private static boolean hasNullableMarker(Schema schema) {
return schema != null && (schema.getNullable() != null ||
schema.getTypes() != null && schema.getTypes().contains("null") ||
schema.getExtensions() != null && schema.getExtensions().containsKey(X_NULLABLE) ||
ModelUtils.isNullableComposedSchema(schema));
}

void updateDefaultToEmptyContainer(CodegenProperty cp, Schema p) {
if (cp.isArray) {
if (!cp.required) { // optional
Expand Down Expand Up @@ -5463,9 +5463,7 @@ public CodegenParameter fromParameter(Parameter parameter, Set<String> imports)
codegenParameter.setTypeProperties(parameterSchema, openAPI);
codegenParameter.setComposedSchemas(getComposedSchemas(parameterSchema));

if (Boolean.TRUE.equals(parameterSchema.getNullable())) { // use nullable defined in the spec
codegenParameter.isNullable = true;
}
codegenParameter.isNullable = ModelUtils.isNullable(parameterSchema);

if (parameter.getStyle() != null) {
codegenParameter.style = parameter.getStyle().toString();
Expand Down Expand Up @@ -8221,10 +8219,8 @@ public CodegenParameter fromRequestBody(RequestBody body, Set<String> imports, S
// restore original schema with description, extensions etc
if (original != null) {
// evaluate common attributes such as description if defined in the top level
if (original.getNullable() != null) {
codegenParameter.isNullable = original.getNullable();
} else if (original.getExtensions() != null && original.getExtensions().containsKey(X_NULLABLE)) {
codegenParameter.isNullable = (Boolean) original.getExtensions().get(X_NULLABLE);
if (hasNullableMarker(original)) {
codegenParameter.isNullable = ModelUtils.isNullable(original);
}

if (original.getExtensions() != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -826,13 +826,10 @@ private void flattenProperties(OpenAPI openAPI, Map<String, Schema> properties,
Schema model = modelFromProperty(openAPI, op, modelName);
String existing = matchGenerated(model);
if (existing != null) {
Schema schema = new Schema().$ref(existing);
schema.setRequired(op.getRequired());
propsToUpdate.put(key, schema);
propsToUpdate.put(key, makeSchema(existing, op));
} else {
modelName = addSchemas(modelName, model);
Schema schema = new Schema().$ref(modelName);
schema.setRequired(op.getRequired());
Schema schema = makeSchema(modelName, op);
propsToUpdate.put(key, schema);
modelsToAdd.put(modelName, model);
}
Expand All @@ -846,23 +843,17 @@ private void flattenProperties(OpenAPI openAPI, Map<String, Schema> properties,
Schema innerModel = modelFromProperty(openAPI, op, modelName);
String existing = matchGenerated(innerModel);
if (existing != null) {
Schema schema = new Schema().$ref(existing);
schema.setRequired(op.getRequired());
property.setItems(schema);
property.setItems(makeSchema(existing, op));
} else {
modelName = addSchemas(modelName, innerModel);
Schema schema = new Schema().$ref(modelName);
schema.setRequired(op.getRequired());
property.setItems(schema);
property.setItems(makeSchema(modelName, op));
}
}
} else if (ModelUtils.isComposedSchema(inner)) {
String innerModelName = resolveModelName(inner.getTitle(), path + "_" + key);
gatherInlineModels(inner, innerModelName);
innerModelName = addSchemas(innerModelName, inner);
Schema schema = new Schema().$ref(innerModelName);
schema.setRequired(inner.getRequired());
property.setItems(schema);
property.setItems(makeSchema(innerModelName, inner));
} else {
LOGGER.debug("Schema not yet handled in model resolver: {}", inner);
}
Expand All @@ -876,23 +867,17 @@ private void flattenProperties(OpenAPI openAPI, Map<String, Schema> properties,
Schema innerModel = modelFromProperty(openAPI, op, modelName);
String existing = matchGenerated(innerModel);
if (existing != null) {
Schema schema = new Schema().$ref(existing);
schema.setRequired(op.getRequired());
property.setAdditionalProperties(schema);
property.setAdditionalProperties(makeSchema(existing, op));
} else {
modelName = addSchemas(modelName, innerModel);
Schema schema = new Schema().$ref(modelName);
schema.setRequired(op.getRequired());
property.setAdditionalProperties(schema);
property.setAdditionalProperties(makeSchema(modelName, op));
}
}
} else if (ModelUtils.isComposedSchema(inner)) {
String innerModelName = resolveModelName(inner.getTitle(), path + "_" + key);
gatherInlineModels(inner, innerModelName);
innerModelName = addSchemas(innerModelName, inner);
Schema schema = new Schema().$ref(innerModelName);
schema.setRequired(inner.getRequired());
property.setAdditionalProperties(schema);
property.setAdditionalProperties(makeSchema(innerModelName, inner));
} else {
LOGGER.debug("Schema not yet handled in model resolver: {}", inner);
}
Expand All @@ -907,9 +892,7 @@ private void flattenProperties(OpenAPI openAPI, Map<String, Schema> properties,
String propertyModelName = resolveModelName(property.getTitle(), path + "_" + key);
gatherInlineModels(property, propertyModelName);
propertyModelName = addSchemas(propertyModelName, property);
Schema schema = new Schema().$ref(propertyModelName);
schema.setRequired(property.getRequired());
propsToUpdate.put(key, schema);
propsToUpdate.put(key, makeSchema(propertyModelName, property));
}
} else {
LOGGER.debug("Schema not yet handled in model resolver: {}", property);
Expand Down Expand Up @@ -1007,6 +990,9 @@ private Schema makeSchemaInComponents(String name, Schema schema) {
refSchema = new Schema().$ref(name);
}
this.copyVendorExtensions(schema, refSchema);
if (ModelUtils.isNullable(schema)) {
refSchema.setNullable(true);
}

return refSchema;
}
Expand All @@ -1020,6 +1006,10 @@ private Schema makeSchemaInComponents(String name, Schema schema) {
*/
private Schema makeSchema(String ref, Schema property) {
Schema newProperty = new Schema().$ref(ref);
newProperty.setRequired(property.getRequired());
if (ModelUtils.isNullable(property)) {
newProperty.setNullable(true);
}
this.copyVendorExtensions(property, newProperty);
return newProperty;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1767,9 +1767,10 @@ public static boolean isExtensionParent(Schema schema) {
* Return true if the 'nullable' attribute is set to true in the schema, i.e. if the value
* of the property can be the null value.
* <p>
* In addition, if the OAS document is 3.1 or above, isNullable returns true if the input
* schema is a 'oneOf' composed document with at most two children, and one of the children
* is the 'null' type.
* In addition, isNullable returns true if the input schema uses JSON Schema nullability
* forms adopted by OAS 3.1: a 'oneOf' composed document with at most two children where
* one child is the 'null' type, or a type-array form where one of the types is 'null'.
* This method checks schema shape only and does not validate the OpenAPI document version.
* <p>
Comment on lines 1769 to 1774
* The caller is responsible for resolving schema references before invoking isNullable.
* If the input schema is a $ref and the referenced schema has 'nullable: true', this method
Expand All @@ -1794,6 +1795,9 @@ public static boolean isNullable(Schema schema) {
if (schema.getExtensions() != null && schema.getExtensions().get(X_NULLABLE) != null) {
return Boolean.parseBoolean(schema.getExtensions().get(X_NULLABLE).toString());
}
if (schema.getTypes() != null && schema.getTypes().contains("null")) {
return true;
}
// In OAS 3.1, the recommended way to define a nullable property or object is to use oneOf.
if (isComposedSchema(schema)) {
return isNullableComposedSchema(schema);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.openapitools.codegen.validations.oas;

import io.swagger.v3.oas.models.media.Schema;
import org.openapitools.codegen.CodegenConstants;
import org.openapitools.codegen.utils.ModelUtils;
import org.openapitools.codegen.utils.SemVer;
import org.openapitools.codegen.validation.GenericValidator;
Expand Down Expand Up @@ -120,7 +121,7 @@ private static ValidationRule.Result checkNullableAttribute(SchemaWrapper schema
if (schemaWrapper.getOpenAPI() != null) {
SemVer version = new SemVer(schemaWrapper.getOpenAPI().getOpenapi());
if (version.atLeast("3.1")) {
if (ModelUtils.isNullable(schema)) {
if (usesNullableAttribute(schema)) {
result = new ValidationRule.Fail();
result.setDetails(String.format(Locale.ROOT,
"OAS document is version '%s'. Schema '%s' uses 'nullable' attribute, which has been deprecated in OAS 3.1.",
Expand All @@ -132,6 +133,18 @@ private static ValidationRule.Result checkNullableAttribute(SchemaWrapper schema
return result;
}

private static boolean usesNullableAttribute(Schema schema) {
if (schema == null) {
return false;
}
if (Boolean.TRUE.equals(schema.getNullable())) {
return true;
}
return schema.getExtensions() != null
&& schema.getExtensions().get(CodegenConstants.X_NULLABLE) != null
&& Boolean.parseBoolean(schema.getExtensions().get(CodegenConstants.X_NULLABLE).toString());
}

private static String nameOf(Schema schema) {
return schema.getName() != null ? schema.getName() : schema.getTitle();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2462,6 +2462,25 @@ public void schemaMappingWithNullableAllOfProperty() {
"dataType must resolve to the referenced schema name");
}

@Test
public void nullableTypeArrayProperty() {
DefaultCodegen codegen = new DefaultCodegen();
OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_1/dart-dio/issue_23866.yaml");
codegen.setOpenAPI(openAPI);

CodegenModel model = codegen.fromModel(
"EnvelopeNullableLoginResponse",
openAPI.getComponents().getSchemas().get("EnvelopeNullableLoginResponse"));

CodegenProperty data = model.vars.stream()
.filter(v -> "data".equals(v.name))
.findFirst()
.orElseThrow(() -> new AssertionError("data property not found"));

assertTrue(data.isNullable,
"data must be nullable because its OAS 3.1 type array includes null");
}

@Test
public void operationIdNameMapping() {
DefaultCodegen codegen = new DefaultCodegen();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -520,4 +520,26 @@ public void nestedArrayItemsCanBeNullable() {

Assert.assertEquals(codegen.getTypeDeclaration(schema), "BuiltList<BuiltList<String?>>");
}

@Test(description = "OAS 3.1 nullable inline object properties keep nullable field type")
public void nullableInlineObjectPropertyWithTypeArray() {
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_1/dart-dio/issue_23866.yaml");
final DartDioClientCodegen codegen = new DartDioClientCodegen();
codegen.additionalProperties().put(CodegenConstants.SERIALIZATION_LIBRARY, DartDioClientCodegen.SERIALIZATION_LIBRARY_BUILT_VALUE);
codegen.processOpts();
codegen.setOpenAPI(openAPI);

final Schema model = openAPI.getComponents().getSchemas().get("EnvelopeNullableLoginResponse");
final CodegenModel cm = codegen.fromModel("EnvelopeNullableLoginResponse", model);

final CodegenProperty data = cm.vars.stream()
.filter(property -> "data".equals(property.baseName))
.findFirst()
.orElseThrow(() -> new AssertionError("data property not found"));

Assert.assertEquals(data.dataType, "EnvelopeNullableLoginResponseData");
Assert.assertEquals(data.datatypeWithEnum, "EnvelopeNullableLoginResponseData");
Assert.assertTrue(data.required);
Assert.assertTrue(data.isNullable);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;

Expand Down Expand Up @@ -615,6 +616,14 @@ public void simplifyOneOfAnyOfWithOnlyOneNonNullSubSchemaKeepsParentDescription(
assertEquals(anyOfSchemaWithChildDescription.getDescription(), "Child description");
}

@Test
public void isNullableWithTypeArrayNull() {
Schema<?> schema = new Schema<>();
schema.setTypes(new LinkedHashSet<>(Arrays.asList("object", "null")));

assertTrue(ModelUtils.isNullable(schema));
}

@Test
public void isNullTypeSchemaTest() {
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/null_schema_test.yaml");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.openapitools.codegen.validations.oas;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.media.*;
import org.openapitools.codegen.validation.Invalid;
import org.openapitools.codegen.validation.ValidationResult;
Expand All @@ -8,6 +9,7 @@
import org.testng.annotations.Test;

import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -67,6 +69,26 @@ public void testOneOfWithSiblingPropertiesDisabledRule(Schema schema, boolean ma
Assert.assertEquals(warnings.size(), 0, "Expected rule to be disabled.");
}

@Test(description = "OAS 3.1 type-array null is not the deprecated nullable attribute")
public void testNullableAttributeRecommendationIgnoresTypeArrayNull() {
RuleConfiguration config = new RuleConfiguration();
config.setEnableRecommendations(true);
OpenApiSchemaValidations validator = new OpenApiSchemaValidations(config);

OpenAPI openAPI = new OpenAPI().openapi("3.1.0");
Schema schema = new Schema();
schema.setTypes(new LinkedHashSet<>(Arrays.asList("object", "null")));

ValidationResult result = validator.validate(new SchemaWrapper(openAPI, schema));
Assert.assertNotNull(result.getWarnings());

List<Invalid> warnings = result.getWarnings().stream()
.filter(invalid -> "Schema uses the 'nullable' attribute.".equals(invalid.getRule().getDescription()))
.collect(Collectors.toList());

Assert.assertEquals(warnings.size(), 0, "Expected type-array null not to trigger nullable attribute recommendation.");
}

@DataProvider(name = "apacheNginxRecommendationExpectations")
public Object[][] apacheNginxRecommendationExpectations() {
return new Object[][]{
Expand Down Expand Up @@ -128,4 +150,4 @@ private ComposedSchema getAnyOfSample(boolean withProperties) {

return schema;
}
}
}
Loading