diff --git a/docs/generators/dart-dio.md b/docs/generators/dart-dio.md
index 7e62f92f0782..ad2785b266d2 100644
--- a/docs/generators/dart-dio.md
+++ b/docs/generators/dart-dio.md
@@ -26,6 +26,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|equalityCheckMethod|Specify equality check method. Takes effect only in case if serializationLibrary is json_serializable.|
- **default**
- [DEFAULT] Built in hash code generation method
- **equatable**
- Uses equatable library for equality checking
|default|
|finalProperties|Whether properties are marked as final when using Json Serializable for serialization| |true|
|legacyDiscriminatorBehavior|Set to false for generators with better support for discriminators. (Python, Java, Go, PowerShell, C# have this enabled by default).|- **true**
- The mapping in the discriminator includes descendent schemas that allOf inherit from self and the discriminator mapping schemas in the OAS document.
- **false**
- The mapping in the discriminator includes any descendent schemas that allOf inherit from self, any oneOf schemas, any anyOf schemas, any x-discriminator-values, and the discriminator mapping schemas in the OAS document AND Codegen validates that oneOf and anyOf schemas contain the required discriminator and throws an error if the discriminator is missing.
|true|
+|patchOnly|Only apply Optional<T> to PATCH operation request bodies (requires useOptional=true)| |false|
|prependFormOrBodyParameters|Add form or body parameters to the beginning of the parameter list.| |false|
|pubAuthor|Author name in generated pubspec| |Author|
|pubAuthorEmail|Email address of the author in generated pubspec| |author@homepage|
@@ -42,6 +43,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|sortParamsByRequiredFlag|Sort method arguments to place required parameters before optional parameters.| |true|
|sourceFolder|source folder for generated code| |src|
|useEnumExtension|Allow the 'x-enum-values' extension for enums| |false|
+|useOptional|Use Optional<T> to distinguish absent, null, and present for optional fields (Dart 3+)| |false|
## IMPORT MAPPING
diff --git a/docs/generators/dart.md b/docs/generators/dart.md
index 79148a7e4791..746afd0a64d0 100644
--- a/docs/generators/dart.md
+++ b/docs/generators/dart.md
@@ -23,6 +23,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|ensureUniqueParams|Whether to ensure parameter names are unique in an operation (rename parameters that are not).| |true|
|enumUnknownDefaultCase|If the server adds new enum cases, that are unknown by an old spec/client, the client will fail to parse the network response.With this option enabled, each enum will have a new case, 'unknown_default_open_api', so that when the server sends an enum case that is not known by the client/spec, they can safely fallback to this case.|- **false**
- No changes to the enum's are made, this is the default option.
- **true**
- With this option enabled, each enum will have a new case, 'unknown_default_open_api', so that when the enum case sent by the server is not known by the client/spec, can safely be decoded to this case.
|false|
|legacyDiscriminatorBehavior|Set to false for generators with better support for discriminators. (Python, Java, Go, PowerShell, C# have this enabled by default).|- **true**
- The mapping in the discriminator includes descendent schemas that allOf inherit from self and the discriminator mapping schemas in the OAS document.
- **false**
- The mapping in the discriminator includes any descendent schemas that allOf inherit from self, any oneOf schemas, any anyOf schemas, any x-discriminator-values, and the discriminator mapping schemas in the OAS document AND Codegen validates that oneOf and anyOf schemas contain the required discriminator and throws an error if the discriminator is missing.
|true|
+|patchOnly|Only apply Optional<T> to PATCH operation request bodies (requires useOptional=true)| |false|
|prependFormOrBodyParameters|Add form or body parameters to the beginning of the parameter list.| |false|
|pubAuthor|Author name in generated pubspec| |Author|
|pubAuthorEmail|Email address of the author in generated pubspec| |author@homepage|
@@ -38,6 +39,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|sortParamsByRequiredFlag|Sort method arguments to place required parameters before optional parameters.| |true|
|sourceFolder|source folder for generated code| |src|
|useEnumExtension|Allow the 'x-enum-values' extension for enums| |false|
+|useOptional|Use Optional<T> to distinguish absent, null, and present for optional fields (Dart 3+)| |false|
## IMPORT MAPPING
diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractDartCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractDartCodegen.java
index f002ee8d8d6a..1eb4d221cf4a 100644
--- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractDartCodegen.java
+++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractDartCodegen.java
@@ -1,9 +1,12 @@
package org.openapitools.codegen.languages;
import com.google.common.collect.Sets;
+import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.media.StringSchema;
+import io.swagger.v3.oas.models.parameters.Parameter;
+import io.swagger.v3.oas.models.parameters.RequestBody;
import io.swagger.v3.oas.models.servers.Server;
import lombok.Setter;
import org.apache.commons.io.FilenameUtils;
@@ -45,6 +48,8 @@ public abstract class AbstractDartCodegen extends DefaultCodegen {
public static final String PUB_REPOSITORY = "pubRepository";
public static final String PUB_PUBLISH_TO = "pubPublishTo";
public static final String USE_ENUM_EXTENSION = "useEnumExtension";
+ public static final String USE_OPTIONAL = "useOptional";
+ public static final String PATCH_ONLY = "patchOnly";
@Setter protected String pubLibrary = "openapi.api";
@Setter protected String pubName = "openapi";
@@ -56,8 +61,12 @@ public abstract class AbstractDartCodegen extends DefaultCodegen {
@Setter protected String pubRepository = null;
@Setter protected String pubPublishTo = null;
@Setter protected boolean useEnumExtension = false;
+ @Setter protected boolean useOptional = false;
+ @Setter protected boolean patchOnly = false;
@Setter protected String sourceFolder = "src";
protected String libPath = "lib" + File.separator;
+
+ protected Set patchRequestSchemas = new HashSet<>();
protected String apiDocPath = "doc/";
protected String modelDocPath = "doc/";
protected String apiTestPath = "test" + File.separator;
@@ -195,6 +204,8 @@ public AbstractDartCodegen() {
addOption(PUB_REPOSITORY, "Repository in generated pubspec", pubRepository);
addOption(PUB_PUBLISH_TO, "Publish_to in generated pubspec", pubPublishTo);
addOption(USE_ENUM_EXTENSION, "Allow the 'x-enum-values' extension for enums", String.valueOf(useEnumExtension));
+ addOption(USE_OPTIONAL, "Use Optional to distinguish absent, null, and present for optional fields (Dart 3+)", String.valueOf(useOptional));
+ addOption(PATCH_ONLY, "Only apply Optional to PATCH operation request bodies (requires useOptional=true)", String.valueOf(patchOnly));
addOption(CodegenConstants.SOURCE_FOLDER, CodegenConstants.SOURCE_FOLDER_DESC, sourceFolder);
}
@@ -301,6 +312,24 @@ public void processOpts() {
additionalProperties.put(USE_ENUM_EXTENSION, useEnumExtension);
}
+ if (additionalProperties.containsKey(USE_OPTIONAL)) {
+ this.setUseOptional(convertPropertyToBooleanAndWriteBack(USE_OPTIONAL));
+ } else {
+ additionalProperties.put(USE_OPTIONAL, useOptional);
+ }
+
+ if (additionalProperties.containsKey(PATCH_ONLY)) {
+ this.setPatchOnly(convertPropertyToBooleanAndWriteBack(PATCH_ONLY));
+ } else {
+ additionalProperties.put(PATCH_ONLY, patchOnly);
+ }
+
+ if (patchOnly && !useOptional) {
+ LOGGER.warn("patchOnly=true requires useOptional=true. Setting useOptional=true.");
+ this.setUseOptional(true);
+ additionalProperties.put(USE_OPTIONAL, true);
+ }
+
if (additionalProperties.containsKey(CodegenConstants.SOURCE_FOLDER)) {
String srcFolder = (String) additionalProperties.get(CodegenConstants.SOURCE_FOLDER);
this.setSourceFolder(srcFolder.replace('/', File.separatorChar));
@@ -544,6 +573,35 @@ public String getTypeDeclaration(Schema p) {
return super.getTypeDeclaration(p);
}
+ @Override
+ public void preprocessOpenAPI(OpenAPI openAPI) {
+ super.preprocessOpenAPI(openAPI);
+
+ if (patchOnly && openAPI.getPaths() != null) {
+ openAPI.getPaths().forEach((path, pathItem) -> {
+ if (pathItem.getPatch() != null) {
+ Operation patchOp = pathItem.getPatch();
+ if (patchOp.getRequestBody() != null) {
+ RequestBody requestBody = ModelUtils.getReferencedRequestBody(openAPI, patchOp.getRequestBody());
+ if (requestBody != null && requestBody.getContent() != null) {
+ requestBody.getContent().forEach((mediaType, content) -> {
+ if (content.getSchema() != null) {
+ String ref = content.getSchema().get$ref();
+ if (ref != null) {
+ String schemaName = ModelUtils.getSimpleRef(ref);
+ String modelName = toModelName(schemaName);
+ patchRequestSchemas.add(modelName);
+ LOGGER.info("Identified '{}' as PATCH request schema (will use Optional)", modelName);
+ }
+ }
+ });
+ }
+ }
+ }
+ });
+ }
+ }
+
@Override
public String getSchemaType(Schema p) {
String openAPIType = super.getSchemaType(p);
@@ -558,7 +616,49 @@ public String getSchemaType(Schema p) {
@Override
public ModelsMap postProcessModels(ModelsMap objs) {
- return postProcessModelsEnum(objs);
+ objs = postProcessModelsEnum(objs);
+
+ if (useOptional) {
+ for (ModelMap modelMap : objs.getModels()) {
+ CodegenModel model = modelMap.getModel();
+
+ boolean shouldUseOptional;
+
+ if (patchOnly) {
+ shouldUseOptional = patchRequestSchemas.contains(model.classname);
+ } else {
+ Boolean schemaUseOptional = (Boolean) model.vendorExtensions.get("x-use-optional");
+ shouldUseOptional = schemaUseOptional != null && schemaUseOptional;
+ }
+
+ if (shouldUseOptional) {
+ for (CodegenProperty prop : model.vars) {
+ if (!prop.required && !prop.dataType.startsWith("Optional<")) {
+ wrapPropertyWithOptional(prop);
+ }
+ }
+ }
+ }
+ }
+
+ return objs;
+ }
+
+ private void wrapPropertyWithOptional(CodegenProperty property) {
+ property.vendorExtensions.put("x-unwrapped-datatype", property.dataType);
+ property.vendorExtensions.put("x-is-optional", true);
+ property.vendorExtensions.put("x-original-is-number", property.isNumber);
+ property.vendorExtensions.put("x-original-is-integer", property.isInteger);
+
+ boolean hasNullableSuffix = property.dataType.endsWith("?");
+ String baseType = hasNullableSuffix ? property.dataType.substring(0, property.dataType.length() - 1) : property.dataType;
+ property.dataType = "Optional<" + baseType + "?" + ">";
+
+ if (property.datatypeWithEnum != null && !property.datatypeWithEnum.startsWith("Optional<")) {
+ hasNullableSuffix = property.datatypeWithEnum.endsWith("?");
+ baseType = hasNullableSuffix ? property.datatypeWithEnum.substring(0, property.datatypeWithEnum.length() - 1) : property.datatypeWithEnum;
+ property.datatypeWithEnum = "Optional<" + baseType + "?" + ">";
+ }
}
@Override
@@ -623,6 +723,19 @@ public CodegenProperty fromProperty(String name, Schema p, boolean required) {
return property;
}
+ @Override
+ public CodegenParameter fromParameter(Parameter parameter, Set imports) {
+ final CodegenParameter param = super.fromParameter(parameter, imports);
+
+ if (useOptional && param.dataType != null && param.dataType.startsWith("Optional<")) {
+ param.dataType = param.dataType.substring("Optional<".length(), param.dataType.length() - 1);
+ param.vendorExtensions.remove("x-is-optional");
+ param.vendorExtensions.remove("x-unwrapped-datatype");
+ }
+
+ return param;
+ }
+
@Override
public CodegenOperation fromOperation(String path, String httpMethod, Operation operation, List servers) {
final CodegenOperation op = super.fromOperation(path, httpMethod, operation, servers);
@@ -659,6 +772,21 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List ops = operations.getOperation();
for (CodegenOperation op : ops) {
+ if (patchOnly && "PATCH".equalsIgnoreCase(op.httpMethod)) {
+ if (op.bodyParam != null && op.bodyParam.dataType != null) {
+ String modelName = getString(op);
+ patchRequestSchemas.add(modelName);
+ LOGGER.debug("Marked schema '{}' for Optional wrapping (PATCH request body)", modelName);
+ }
+ }
+
+ if (useOptional) {
+ unwrapOptionalFromParameters(op.pathParams);
+ unwrapOptionalFromParameters(op.queryParams);
+ unwrapOptionalFromParameters(op.headerParams);
+ unwrapOptionalFromParameters(op.formParams);
+ }
+
if (op.hasConsumes) {
if (!op.formParams.isEmpty() || op.isMultipart) {
// DefaultCodegen only sets this if the first consumes mediaType
@@ -680,6 +808,29 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List');
+ if (start > 0 && end > start) {
+ modelName = modelName.substring(start, end);
+ }
+ }
+ modelName = modelName.replace("?", "");
+ return modelName;
+ }
+
+ private void unwrapOptionalFromParameters(List params) {
+ if (params == null) return;
+ for (CodegenParameter param : params) {
+ if (param.dataType != null && param.dataType.startsWith("Optional<")) {
+ param.dataType = param.dataType.substring("Optional<".length(), param.dataType.length() - 1);
+ param.vendorExtensions.remove("x-is-optional");
+ }
+ }
+ }
+
private List