11package org .openapitools .codegen .languages ;
22
33import com .google .common .collect .Sets ;
4+ import io .swagger .v3 .oas .models .OpenAPI ;
45import io .swagger .v3 .oas .models .Operation ;
56import io .swagger .v3 .oas .models .media .Schema ;
67import io .swagger .v3 .oas .models .media .StringSchema ;
8+ import io .swagger .v3 .oas .models .parameters .Parameter ;
9+ import io .swagger .v3 .oas .models .parameters .RequestBody ;
710import io .swagger .v3 .oas .models .servers .Server ;
811import lombok .Setter ;
912import org .apache .commons .io .FilenameUtils ;
@@ -45,6 +48,8 @@ public abstract class AbstractDartCodegen extends DefaultCodegen {
4548 public static final String PUB_REPOSITORY = "pubRepository" ;
4649 public static final String PUB_PUBLISH_TO = "pubPublishTo" ;
4750 public static final String USE_ENUM_EXTENSION = "useEnumExtension" ;
51+ public static final String USE_OPTIONAL = "useOptional" ;
52+ public static final String PATCH_ONLY = "patchOnly" ;
4853
4954 @ Setter protected String pubLibrary = "openapi.api" ;
5055 @ Setter protected String pubName = "openapi" ;
@@ -56,8 +61,12 @@ public abstract class AbstractDartCodegen extends DefaultCodegen {
5661 @ Setter protected String pubRepository = null ;
5762 @ Setter protected String pubPublishTo = null ;
5863 @ Setter protected boolean useEnumExtension = false ;
64+ @ Setter protected boolean useOptional = false ;
65+ @ Setter protected boolean patchOnly = false ;
5966 @ Setter protected String sourceFolder = "src" ;
6067 protected String libPath = "lib" + File .separator ;
68+
69+ protected Set <String > patchRequestSchemas = new HashSet <>();
6170 protected String apiDocPath = "doc/" ;
6271 protected String modelDocPath = "doc/" ;
6372 protected String apiTestPath = "test" + File .separator ;
@@ -195,6 +204,8 @@ public AbstractDartCodegen() {
195204 addOption (PUB_REPOSITORY , "Repository in generated pubspec" , pubRepository );
196205 addOption (PUB_PUBLISH_TO , "Publish_to in generated pubspec" , pubPublishTo );
197206 addOption (USE_ENUM_EXTENSION , "Allow the 'x-enum-values' extension for enums" , String .valueOf (useEnumExtension ));
207+ addOption (USE_OPTIONAL , "Use Optional<T> to distinguish absent, null, and present for optional fields (Dart 3+)" , String .valueOf (useOptional ));
208+ addOption (PATCH_ONLY , "Only apply Optional<T> to PATCH operation request bodies (requires useOptional=true)" , String .valueOf (patchOnly ));
198209 addOption (CodegenConstants .SOURCE_FOLDER , CodegenConstants .SOURCE_FOLDER_DESC , sourceFolder );
199210 }
200211
@@ -301,6 +312,24 @@ public void processOpts() {
301312 additionalProperties .put (USE_ENUM_EXTENSION , useEnumExtension );
302313 }
303314
315+ if (additionalProperties .containsKey (USE_OPTIONAL )) {
316+ this .setUseOptional (convertPropertyToBooleanAndWriteBack (USE_OPTIONAL ));
317+ } else {
318+ additionalProperties .put (USE_OPTIONAL , useOptional );
319+ }
320+
321+ if (additionalProperties .containsKey (PATCH_ONLY )) {
322+ this .setPatchOnly (convertPropertyToBooleanAndWriteBack (PATCH_ONLY ));
323+ } else {
324+ additionalProperties .put (PATCH_ONLY , patchOnly );
325+ }
326+
327+ if (patchOnly && !useOptional ) {
328+ LOGGER .warn ("patchOnly=true requires useOptional=true. Setting useOptional=true." );
329+ this .setUseOptional (true );
330+ additionalProperties .put (USE_OPTIONAL , true );
331+ }
332+
304333 if (additionalProperties .containsKey (CodegenConstants .SOURCE_FOLDER )) {
305334 String srcFolder = (String ) additionalProperties .get (CodegenConstants .SOURCE_FOLDER );
306335 this .setSourceFolder (srcFolder .replace ('/' , File .separatorChar ));
@@ -544,6 +573,35 @@ public String getTypeDeclaration(Schema p) {
544573 return super .getTypeDeclaration (p );
545574 }
546575
576+ @ Override
577+ public void preprocessOpenAPI (OpenAPI openAPI ) {
578+ super .preprocessOpenAPI (openAPI );
579+
580+ if (patchOnly && openAPI .getPaths () != null ) {
581+ openAPI .getPaths ().forEach ((path , pathItem ) -> {
582+ if (pathItem .getPatch () != null ) {
583+ Operation patchOp = pathItem .getPatch ();
584+ if (patchOp .getRequestBody () != null ) {
585+ RequestBody requestBody = ModelUtils .getReferencedRequestBody (openAPI , patchOp .getRequestBody ());
586+ if (requestBody != null && requestBody .getContent () != null ) {
587+ requestBody .getContent ().forEach ((mediaType , content ) -> {
588+ if (content .getSchema () != null ) {
589+ String ref = content .getSchema ().get$ref ();
590+ if (ref != null ) {
591+ String schemaName = ModelUtils .getSimpleRef (ref );
592+ String modelName = toModelName (schemaName );
593+ patchRequestSchemas .add (modelName );
594+ LOGGER .info ("Identified '{}' as PATCH request schema (will use Optional<T>)" , modelName );
595+ }
596+ }
597+ });
598+ }
599+ }
600+ }
601+ });
602+ }
603+ }
604+
547605 @ Override
548606 public String getSchemaType (Schema p ) {
549607 String openAPIType = super .getSchemaType (p );
@@ -558,7 +616,49 @@ public String getSchemaType(Schema p) {
558616
559617 @ Override
560618 public ModelsMap postProcessModels (ModelsMap objs ) {
561- return postProcessModelsEnum (objs );
619+ objs = postProcessModelsEnum (objs );
620+
621+ if (useOptional ) {
622+ for (ModelMap modelMap : objs .getModels ()) {
623+ CodegenModel model = modelMap .getModel ();
624+
625+ boolean shouldUseOptional ;
626+
627+ if (patchOnly ) {
628+ shouldUseOptional = patchRequestSchemas .contains (model .classname );
629+ } else {
630+ Boolean schemaUseOptional = (Boolean ) model .vendorExtensions .get ("x-use-optional" );
631+ shouldUseOptional = schemaUseOptional != null && schemaUseOptional ;
632+ }
633+
634+ if (shouldUseOptional ) {
635+ for (CodegenProperty prop : model .vars ) {
636+ if (!prop .required && !prop .dataType .startsWith ("Optional<" )) {
637+ wrapPropertyWithOptional (prop );
638+ }
639+ }
640+ }
641+ }
642+ }
643+
644+ return objs ;
645+ }
646+
647+ private void wrapPropertyWithOptional (CodegenProperty property ) {
648+ property .vendorExtensions .put ("x-unwrapped-datatype" , property .dataType );
649+ property .vendorExtensions .put ("x-is-optional" , true );
650+ property .vendorExtensions .put ("x-original-is-number" , property .isNumber );
651+ property .vendorExtensions .put ("x-original-is-integer" , property .isInteger );
652+
653+ boolean hasNullableSuffix = property .dataType .endsWith ("?" );
654+ String baseType = hasNullableSuffix ? property .dataType .substring (0 , property .dataType .length () - 1 ) : property .dataType ;
655+ property .dataType = "Optional<" + baseType + "?" + ">" ;
656+
657+ if (property .datatypeWithEnum != null && !property .datatypeWithEnum .startsWith ("Optional<" )) {
658+ hasNullableSuffix = property .datatypeWithEnum .endsWith ("?" );
659+ baseType = hasNullableSuffix ? property .datatypeWithEnum .substring (0 , property .datatypeWithEnum .length () - 1 ) : property .datatypeWithEnum ;
660+ property .datatypeWithEnum = "Optional<" + baseType + "?" + ">" ;
661+ }
562662 }
563663
564664 @ Override
@@ -623,6 +723,19 @@ public CodegenProperty fromProperty(String name, Schema p, boolean required) {
623723 return property ;
624724 }
625725
726+ @ Override
727+ public CodegenParameter fromParameter (Parameter parameter , Set <String > imports ) {
728+ final CodegenParameter param = super .fromParameter (parameter , imports );
729+
730+ if (useOptional && param .dataType != null && param .dataType .startsWith ("Optional<" )) {
731+ param .dataType = param .dataType .substring ("Optional<" .length (), param .dataType .length () - 1 );
732+ param .vendorExtensions .remove ("x-is-optional" );
733+ param .vendorExtensions .remove ("x-unwrapped-datatype" );
734+ }
735+
736+ return param ;
737+ }
738+
626739 @ Override
627740 public CodegenOperation fromOperation (String path , String httpMethod , Operation operation , List <Server > servers ) {
628741 final CodegenOperation op = super .fromOperation (path , httpMethod , operation , servers );
@@ -659,6 +772,21 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
659772 if (operations != null ) {
660773 List <CodegenOperation > ops = operations .getOperation ();
661774 for (CodegenOperation op : ops ) {
775+ if (patchOnly && "PATCH" .equalsIgnoreCase (op .httpMethod )) {
776+ if (op .bodyParam != null && op .bodyParam .dataType != null ) {
777+ String modelName = getString (op );
778+ patchRequestSchemas .add (modelName );
779+ LOGGER .debug ("Marked schema '{}' for Optional wrapping (PATCH request body)" , modelName );
780+ }
781+ }
782+
783+ if (useOptional ) {
784+ unwrapOptionalFromParameters (op .pathParams );
785+ unwrapOptionalFromParameters (op .queryParams );
786+ unwrapOptionalFromParameters (op .headerParams );
787+ unwrapOptionalFromParameters (op .formParams );
788+ }
789+
662790 if (op .hasConsumes ) {
663791 if (!op .formParams .isEmpty () || op .isMultipart ) {
664792 // DefaultCodegen only sets this if the first consumes mediaType
@@ -680,6 +808,29 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
680808 return objs ;
681809 }
682810
811+ private static String getString (CodegenOperation op ) {
812+ String modelName = op .bodyParam .dataType ;
813+ if (modelName .startsWith ("List<" ) || modelName .startsWith ("Map<" )) {
814+ int start = modelName .indexOf ('<' ) + 1 ;
815+ int end = modelName .lastIndexOf ('>' );
816+ if (start > 0 && end > start ) {
817+ modelName = modelName .substring (start , end );
818+ }
819+ }
820+ modelName = modelName .replace ("?" , "" );
821+ return modelName ;
822+ }
823+
824+ private void unwrapOptionalFromParameters (List <CodegenParameter > params ) {
825+ if (params == null ) return ;
826+ for (CodegenParameter param : params ) {
827+ if (param .dataType != null && param .dataType .startsWith ("Optional<" )) {
828+ param .dataType = param .dataType .substring ("Optional<" .length (), param .dataType .length () - 1 );
829+ param .vendorExtensions .remove ("x-is-optional" );
830+ }
831+ }
832+ }
833+
683834 private List <Map <String , String >> prioritizeContentTypes (List <Map <String , String >> consumes ) {
684835 if (consumes .size () <= 1 ) {
685836 // no need to change any order
0 commit comments