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 @@ -4323,6 +4316,13 @@ public CodegenProperty fromProperty(String name, Schema p, boolean required, boo
return property;
}

private static boolean hasNullableMarker(Schema schema) {
Comment thread
ShellWen marked this conversation as resolved.
return schema != null && (schema.getNullable() != null ||
ModelUtils.hasType(schema, "null") ||
ModelUtils.hasExtension(schema, X_NULLABLE) ||
ModelUtils.isNullableComposedSchema(schema));
}

/**
* update container's default to empty container according rules provided by the user.
*
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 @@ -418,6 +418,14 @@ public static boolean hasEnum(Schema<?> schema) {
return schema.getEnum() != null && !schema.getEnum().isEmpty();
}

public static boolean hasType(Schema<?> schema, String type) {
return schema != null && schema.getTypes() != null && schema.getTypes().contains(type);
}

public static boolean hasExtension(Schema<?> schema, String extensionName) {
return schema != null && schema.getExtensions() != null && schema.getExtensions().containsKey(extensionName);
}

/**
* Return true if the specified schema is type object
* Only considers OAS 3.0 {@code type} and not OAS 3.1 {@code types}
Expand Down Expand Up @@ -1767,9 +1775,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 1777 to 1782
* 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 @@ -1791,8 +1800,14 @@ public static boolean isNullable(Schema schema) {
return true;
}

if (schema.getExtensions() != null && schema.getExtensions().get(X_NULLABLE) != null) {
return Boolean.parseBoolean(schema.getExtensions().get(X_NULLABLE).toString());
if (hasExtension(schema, X_NULLABLE)) {
Object nullable = schema.getExtensions().get(X_NULLABLE);
if (nullable != null) {
return Boolean.parseBoolean(nullable.toString());
}
}
if (hasType(schema, "null")) {
return true;
}
// In OAS 3.1, the recommended way to define a nullable property or object is to use oneOf.
if (isComposedSchema(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,20 @@ 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;
}
if (ModelUtils.hasExtension(schema, CodegenConstants.X_NULLABLE)) {
Object nullable = schema.getExtensions().get(CodegenConstants.X_NULLABLE);
return nullable != null && Boolean.parseBoolean(nullable.toString());
}
return false;
}

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,36 @@ 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 hasTypeChecksSchemaTypes() {
Schema<?> schema = new Schema<>();
schema.setTypes(new LinkedHashSet<>(Arrays.asList("object", "null")));

assertTrue(ModelUtils.hasType(schema, "null"));
assertFalse(ModelUtils.hasType(schema, "string"));
assertFalse(ModelUtils.hasType(new Schema<>(), "null"));
assertFalse(ModelUtils.hasType(null, "null"));
}

@Test
public void hasExtensionChecksSchemaExtensions() {
Schema<?> schema = new Schema<>();
schema.addExtension("x-nullable", Boolean.TRUE);

assertTrue(ModelUtils.hasExtension(schema, "x-nullable"));
assertFalse(ModelUtils.hasExtension(schema, "x-other"));
assertFalse(ModelUtils.hasExtension(new Schema<>(), "x-nullable"));
assertFalse(ModelUtils.hasExtension(null, "x-nullable"));
}

@Test
public void isNullTypeSchemaTest() {
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/null_schema_test.yaml");
Expand Down
Loading