From 319720aa53020902af27bf3a76cd2322f6b86d42 Mon Sep 17 00:00:00 2001 From: radelcom Date: Tue, 5 May 2026 10:33:41 -0700 Subject: [PATCH 1/4] fix(dart-dio): PATCH tri-state optional handling --- .../codegen/languages/AbstractDartCodegen.java | 4 ++++ .../resources/dart/libraries/dio/lib.mustache | 2 ++ .../dio/serialization/built_value/class.mustache | 2 ++ .../built_value/class_members.mustache | 5 +++-- .../built_value/class_serializer.mustache | 16 ++++++++++++++++ .../variable_serializer_type.mustache | 2 +- 6 files changed, 28 insertions(+), 3 deletions(-) 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 4d7eb4dbe7c1..72cfbf08a1de 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 @@ -622,6 +622,7 @@ public ModelsMap postProcessModels(ModelsMap objs) { if (useOptional) { for (ModelMap modelMap : objs.getModels()) { CodegenModel model = modelMap.getModel(); + boolean hasOptionalProperties = false; boolean shouldUseOptional; @@ -636,9 +637,12 @@ public ModelsMap postProcessModels(ModelsMap objs) { for (CodegenProperty prop : model.vars) { if (!prop.required && !prop.dataType.startsWith("Optional<")) { wrapPropertyWithOptional(prop); + hasOptionalProperties = true; } } } + + model.vendorExtensions.put("x-has-optional-properties", hasOptionalProperties); } } diff --git a/modules/openapi-generator/src/main/resources/dart/libraries/dio/lib.mustache b/modules/openapi-generator/src/main/resources/dart/libraries/dio/lib.mustache index 880bb6258354..581624c15a8b 100644 --- a/modules/openapi-generator/src/main/resources/dart/libraries/dio/lib.mustache +++ b/modules/openapi-generator/src/main/resources/dart/libraries/dio/lib.mustache @@ -4,6 +4,8 @@ export 'package:{{pubName}}/{{sourceFolder}}/auth/api_key_auth.dart'; export 'package:{{pubName}}/{{sourceFolder}}/auth/basic_auth.dart'; export 'package:{{pubName}}/{{sourceFolder}}/auth/bearer_auth.dart'; export 'package:{{pubName}}/{{sourceFolder}}/auth/oauth.dart'; +{{#useOptional}}export 'package:{{pubName}}/{{sourceFolder}}/optional.dart'; +{{/useOptional}} {{#useBuiltValue}}export 'package:{{pubName}}/{{sourceFolder}}/serializers.dart'; {{#useDateLibCore}}export 'package:{{pubName}}/{{sourceFolder}}/{{modelPackage}}/date.dart';{{/useDateLibCore}}{{/useBuiltValue}} diff --git a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/class.mustache b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/class.mustache index 7a9ee688596b..7aae4bb433b2 100644 --- a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/class.mustache +++ b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/class.mustache @@ -2,6 +2,8 @@ import 'package:built_value/built_value.dart'; import 'package:built_value/serializer.dart';{{#oneOf}}{{#-first}} import 'package:one_of/one_of.dart';{{/-first}}{{/oneOf}}{{#anyOf}}{{#-first}} import 'package:one_of/any_of.dart';{{/-first}}{{/anyOf}} +{{#vendorExtensions.x-has-optional-properties}} +import 'package:{{pubName}}/{{sourceFolder}}/optional.dart';{{/vendorExtensions.x-has-optional-properties}} {{#imports}} {{/imports}} diff --git a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/class_members.mustache b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/class_members.mustache index 50b7f98ba60d..f8cbd467ab05 100644 --- a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/class_members.mustache +++ b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/class_members.mustache @@ -31,8 +31,9 @@ factory {{classname}}([void updates({{classname}}Builder b)]) = _${{classname}}; @BuiltValueHook(initializeBuilder: true) - static void _defaults({{{classname}}}Builder b) => b{{#vendorExtensions.x-parent-discriminator}}..{{propertyName}}=b.discriminatorValue{{/vendorExtensions.x-parent-discriminator}}{{#vendorExtensions.x-self-and-ancestor-only-props}}{{#defaultValue}} - ..{{{name}}} = {{#isEnum}}{{^isContainer}}{{#enumName}}{{enumName}}.valueOf({{{defaultValue}}}){{/enumName}}{{^enumName}}{{{defaultValue}}}{{/enumName}}{{/isContainer}}{{/isEnum}}{{^isEnum}}{{{defaultValue}}}{{/isEnum}}{{/defaultValue}}{{/vendorExtensions.x-self-and-ancestor-only-props}}; + static void _defaults({{{classname}}}Builder b) => b{{#vendorExtensions.x-parent-discriminator}}..{{propertyName}}=b.discriminatorValue{{/vendorExtensions.x-parent-discriminator}}{{#vendorExtensions.x-self-and-ancestor-only-props}}{{#vendorExtensions.x-is-optional}} + ..{{{name}}} = Optional.absent(){{/vendorExtensions.x-is-optional}}{{^vendorExtensions.x-is-optional}}{{#defaultValue}} + ..{{{name}}} = {{#isEnum}}{{^isContainer}}{{#enumName}}{{enumName}}.valueOf({{{defaultValue}}}){{/enumName}}{{^enumName}}{{{defaultValue}}}{{/enumName}}{{/isContainer}}{{/isEnum}}{{^isEnum}}{{{defaultValue}}}{{/isEnum}}{{/defaultValue}}{{/vendorExtensions.x-is-optional}}{{/vendorExtensions.x-self-and-ancestor-only-props}}; {{/vendorExtensions.x-is-parent}} @BuiltValueSerializer(custom: true) static Serializer<{{classname}}> get serializer => _${{classname}}Serializer(); \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/class_serializer.mustache b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/class_serializer.mustache index 4cd02f3506de..ef0a6301bfa3 100644 --- a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/class_serializer.mustache +++ b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/class_serializer.mustache @@ -11,6 +11,21 @@ class _${{classname}}Serializer implements PrimitiveSerializer<{{classname}}> { FullType specifiedType = FullType.unspecified, }) sync* { {{#vendorExtensions.x-self-and-ancestor-only-props}} + {{#vendorExtensions.x-is-optional}} + if (object.{{{name}}}.isPresent) { + yield r'{{baseName}}'; + final optionalValue = object.{{{name}}}.value; + if (optionalValue == null) { + yield null; + } else { + yield serializers.serialize( + optionalValue, + specifiedType: const {{>serialization/built_value/variable_serializer_type}}, + ); + } + } + {{/vendorExtensions.x-is-optional}} + {{^vendorExtensions.x-is-optional}} {{#required}} {{! A required property need to always be part of the serialized output. @@ -32,6 +47,7 @@ class _${{classname}}Serializer implements PrimitiveSerializer<{{classname}}> { ); } {{/required}} + {{/vendorExtensions.x-is-optional}} {{/vendorExtensions.x-self-and-ancestor-only-props}} } diff --git a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/variable_serializer_type.mustache b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/variable_serializer_type.mustache index c76dde39a9c9..8116fcb4ede8 100644 --- a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/variable_serializer_type.mustache +++ b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/variable_serializer_type.mustache @@ -1 +1 @@ -FullType{{#isNullable}}.nullable{{/isNullable}}({{#isContainer}}{{baseType}}, [{{#isMap}}FullType(String), {{/isMap}}{{#items}}{{>serialization/built_value/variable_serializer_type}}{{/items}}]{{/isContainer}}{{^isContainer}}{{{datatypeWithEnum}}}{{/isContainer}}) \ No newline at end of file +{{#vendorExtensions.x-is-optional}}{{#isContainer}}FullType{{#isNullable}}.nullable{{/isNullable}}({{baseType}}, [{{#isMap}}FullType(String), {{/isMap}}{{#items}}{{>serialization/built_value/variable_serializer_type}}{{/items}}]){{/isContainer}}{{^isContainer}}FullType.nullable({{{vendorExtensions.x-unwrapped-datatype}}}){{/isContainer}}{{/vendorExtensions.x-is-optional}}{{^vendorExtensions.x-is-optional}}FullType{{#isNullable}}.nullable{{/isNullable}}({{#isContainer}}{{baseType}}, [{{#isMap}}FullType(String), {{/isMap}}{{#items}}{{>serialization/built_value/variable_serializer_type}}{{/items}}]{{/isContainer}}{{^isContainer}}{{{datatypeWithEnum}}}{{/isContainer}}){{/vendorExtensions.x-is-optional}} \ No newline at end of file From 6c2ac879101eddd7053db71cf1a756a877861ae7 Mon Sep 17 00:00:00 2001 From: Jeffrey Oloresisimo Date: Tue, 5 May 2026 14:58:52 -0700 Subject: [PATCH 2/4] fix(dart-dio): avoid whitespace-only sample churn in class template --- .../libraries/dio/serialization/built_value/class.mustache | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/class.mustache b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/class.mustache index 7aae4bb433b2..b7ce07ace304 100644 --- a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/class.mustache +++ b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/class.mustache @@ -2,8 +2,8 @@ import 'package:built_value/built_value.dart'; import 'package:built_value/serializer.dart';{{#oneOf}}{{#-first}} import 'package:one_of/one_of.dart';{{/-first}}{{/oneOf}}{{#anyOf}}{{#-first}} import 'package:one_of/any_of.dart';{{/-first}}{{/anyOf}} -{{#vendorExtensions.x-has-optional-properties}} -import 'package:{{pubName}}/{{sourceFolder}}/optional.dart';{{/vendorExtensions.x-has-optional-properties}} +{{#vendorExtensions.x-has-optional-properties}}import 'package:{{pubName}}/{{sourceFolder}}/optional.dart'; +{{/vendorExtensions.x-has-optional-properties}} {{#imports}} {{/imports}} From e47a30a6e2bbee456cce3644c5eb23fb3c083183 Mon Sep 17 00:00:00 2001 From: radelcom Date: Wed, 6 May 2026 08:07:05 -0700 Subject: [PATCH 3/4] fix(dart-dio): remove trailing ? from Optional-wrapped fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The outer Optional should never be null—only the inner value can be null. Field type should be Optional, not Optional? This fixes compilation errors when serializer tries to access .isPresent on a potentially null Optional. --- .../dio/serialization/built_value/variable_type.mustache | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/variable_type.mustache b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/variable_type.mustache index 136545525cac..78ca08f2a1c6 100644 --- a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/variable_type.mustache +++ b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/variable_type.mustache @@ -1 +1 @@ -{{#isContainer}}{{baseType}}<{{#isMap}}String, {{/isMap}}{{#items}}{{>serialization/built_value/variable_type}}{{/items}}>{{/isContainer}}{{^isContainer}}{{{datatypeWithEnum}}}{{/isContainer}}{{#isNullable}}?{{/isNullable}} \ No newline at end of file +{{#isContainer}}{{baseType}}<{{#isMap}}String, {{/isMap}}{{#items}}{{>serialization/built_value/variable_type}}{{/items}}>{{/isContainer}}{{^isContainer}}{{{datatypeWithEnum}}}{{/isContainer}}{{#isNullable}}{{^vendorExtensions.x-is-optional}}?{{/vendorExtensions.x-is-optional}}{{/isNullable}} \ No newline at end of file From 70b3b616f856977b0513e82d14630d423860e4d2 Mon Sep 17 00:00:00 2001 From: radelcom Date: Wed, 6 May 2026 08:49:43 -0700 Subject: [PATCH 4/4] fix: more testings which ended up actually needing the deserialization part --- .../codegen/languages/AbstractDartCodegen.java | 3 +++ .../main/resources/dart/libraries/dio/optional.mustache | 6 +++--- .../dio/serialization/built_value/class_members.mustache | 2 +- .../built_value/deserialize_properties.mustache | 9 +++++++++ 4 files changed, 16 insertions(+), 4 deletions(-) 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 72cfbf08a1de..4389714fa119 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 @@ -657,6 +657,7 @@ private void wrapPropertyWithOptional(CodegenProperty property) { boolean hasNullableSuffix = property.dataType.endsWith("?"); String baseType = hasNullableSuffix ? property.dataType.substring(0, property.dataType.length() - 1) : property.dataType; + property.vendorExtensions.put("x-unwrapped-datatype-nullable", baseType + "?"); property.dataType = "Optional<" + baseType + "?" + ">"; if (property.datatypeWithEnum != null && !property.datatypeWithEnum.startsWith("Optional<")) { @@ -664,6 +665,8 @@ private void wrapPropertyWithOptional(CodegenProperty property) { baseType = hasNullableSuffix ? property.datatypeWithEnum.substring(0, property.datatypeWithEnum.length() - 1) : property.datatypeWithEnum; property.datatypeWithEnum = "Optional<" + baseType + "?" + ">"; } + + property.isNullable = false; } @Override diff --git a/modules/openapi-generator/src/main/resources/dart/libraries/dio/optional.mustache b/modules/openapi-generator/src/main/resources/dart/libraries/dio/optional.mustache index 9a2747da4e52..f4fa6f8f2586 100644 --- a/modules/openapi-generator/src/main/resources/dart/libraries/dio/optional.mustache +++ b/modules/openapi-generator/src/main/resources/dart/libraries/dio/optional.mustache @@ -19,7 +19,7 @@ import 'package:json_annotation/json_annotation.dart'; /// // Field has value - sends {"field": "value"} /// final patch3 = Model(field: const Optional.present('value')); /// ``` -sealed class Optional { +abstract class Optional { const Optional(); /// Creates an Optional with an absent value (not set). @@ -48,7 +48,7 @@ sealed class Optional { } /// Represents an absent Optional value. -final class Absent extends Optional { +class Absent extends Optional { const Absent(); @override @@ -77,7 +77,7 @@ final class Absent extends Optional { } /// Represents a present Optional value. -final class Present extends Optional { +class Present extends Optional { const Present(this._value); final T _value; diff --git a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/class_members.mustache b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/class_members.mustache index f8cbd467ab05..cfc50dd82b87 100644 --- a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/class_members.mustache +++ b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/class_members.mustache @@ -7,7 +7,7 @@ @Deprecated('{{{name}}} has been deprecated') {{/deprecated}} @BuiltValueField(wireName: r'{{baseName}}') - {{>serialization/built_value/variable_type}}{{^isNullable}}{{^required}}?{{/required}}{{/isNullable}} get {{name}}; + {{>serialization/built_value/variable_type}}{{^vendorExtensions.x-is-optional}}{{^isNullable}}{{^required}}?{{/required}}{{/isNullable}}{{/vendorExtensions.x-is-optional}} get {{name}}; {{#allowableValues}} // {{#min}}range from {{{min}}} to {{{max}}}{{/min}}{{^min}}enum {{name}}Enum { {{#values}} {{{.}}}, {{/values}} };{{/min}} {{/allowableValues}} diff --git a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/deserialize_properties.mustache b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/deserialize_properties.mustache index d64ec8086c34..b04145f5b5a8 100644 --- a/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/deserialize_properties.mustache +++ b/modules/openapi-generator/src/main/resources/dart/libraries/dio/serialization/built_value/deserialize_properties.mustache @@ -12,6 +12,14 @@ switch (key) { {{#vendorExtensions.x-self-and-ancestor-only-props}} case r'{{baseName}}': + {{#vendorExtensions.x-is-optional}} + final valueDes = serializers.deserialize( + value, + specifiedType: const {{>serialization/built_value/variable_serializer_type}}, + ) as {{{vendorExtensions.x-unwrapped-datatype-nullable}}}; + result.{{{name}}} = Optional.present(valueDes); + {{/vendorExtensions.x-is-optional}} + {{^vendorExtensions.x-is-optional}} final valueDes = serializers.deserialize( value, specifiedType: const {{>serialization/built_value/variable_serializer_type}}, @@ -46,6 +54,7 @@ {{/isModel}} {{/isEnum}} {{/isContainer}} + {{/vendorExtensions.x-is-optional}} break; {{/vendorExtensions.x-self-and-ancestor-only-props}} default: