Skip to content

[BUG][DART] Fix anyOf undeclared enum, empty class syntax, and nested map cast#23671

Open
winrid wants to merge 3 commits intoOpenAPITools:masterfrom
winrid:pr/dart-anyof-empty-class-nested-map
Open

[BUG][DART] Fix anyOf undeclared enum, empty class syntax, and nested map cast#23671
winrid wants to merge 3 commits intoOpenAPITools:masterfrom
winrid:pr/dart-anyof-empty-class-nested-map

Conversation

@winrid
Copy link
Copy Markdown
Contributor

@winrid winrid commented Apr 30, 2026

Closes #23665, closes #16715. Refs #9272, #12914, #15670.

Three dart generator bugs surfaced from the same end-user spec, fixed together because they share the same template + sample regen surface.

1. anyOf of $ref enum + inline literal enum emits undeclared StatusEnum (closes #23665)

status:
  anyOf:
    - \$ref: '#/components/schemas/APIStatus'
    - { type: string, enum: ['pending-verification'] }

Previously emitted StatusEnum? status plus StatusEnum.fromJson(...), but StatusEnum was never declared anywhere in lib/model/. The dart generator can't produce a single enum class that covers all branches, and the inline-enum template is suppressed for composed schemas.

Fix: in AbstractDartCodegen.fromProperty, when the composed schema has 2+ branches and the default codegen guessed at isEnum=true, drop the enum projection (clear isEnum, enumName, _enum, allowableValues; reset datatypeWithEnum=dataType). Single-element composed schemas are unchanged. Output is now String? status with mapValueOfType<String>(...) deserialization.

2. Empty-property classes emit invalid Dart syntax (closes #16715)

A schema resolving to a class with zero properties produced:

  • Foo({ }) (Dart rejects empty named-args lists)
  • a dangling && on == with no rhs and no ;
  • a missing rhs on hashCode

Fix: {{#hasVars}} guards in dart_constructor.mustache and native_class.mustache. Empty classes now emit Foo(), bool operator ==(Object other) => identical(this, other) || other is Foo;, and int get hashCode => 0;.

3. Map<String, Map<String, T>> decoded with wrong generic args (refs #9272, #12914, #15670)

myReacts:
  type: object
  additionalProperties:
    type: object
    additionalProperties:
      type: boolean

Previously emitted mapCastOfType<String, dynamic>(json, r'myReacts') which returns Map<String, dynamic> and can't be assigned to the declared field type Map<String, Map<String, bool>>.

Fix: native_class.mustache's nested-map branch now emits

(json[r'X'] as Map).map((k, v) => MapEntry(k as String, (v as Map).cast<String, T>()))

This also fixes the same broken pattern that was already in the pre-existing additional_properties_class.dart and map_test.dart samples.

Reproducers + verification

PetReactionStatus, PetReactionResponse, PetEmptyMetadata, PetReactionsResponse added to the dart fake-petstore yaml as red-green test fixtures. Pre-fix dart analyze reported each bug verbatim (undefined StatusEnum, empty-class syntax errors, Map<String, dynamic> not assignable). Post-fix all three model files are clean; remaining errors in the fake sample are pre-existing and unrelated to these bugs. Plain petstore_client_lib sample analyzes clean. All 88 org.openapitools.codegen.dart.* Java tests pass.

PR checklist

  • Read the contribution guidelines.
  • Pull Request title clearly describes the work in the pull request and Pull Request description provides details about how to validate the work. Missing information here may result in delayed response from the community.
  • Run the following to build the project and update samples:
    ./mvnw clean package
    
  • File the PR against the correct branch: master (5.x.x) (patches will be cascaded from master to other branches by the maintainers)
  • If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.

cc @jaumard @amondnet @sbu-WBT @kuhnroyal @agilob @ahmednfwela

… map cast

Three bugs in the dart generator surfaced from the same end-user spec.
The repros are added to the dart fake-petstore yaml as PetReactionStatus,
PetReactionResponse, PetEmptyMetadata, PetReactionsResponse so each can
be reproduced and red/green tested against generated output.

1. anyOf of [$ref enum, inline literal enum] emitted a property typed as
   `StatusEnum?` plus `StatusEnum.fromJson(...)`, but never declared the
   enum class anywhere in lib/model/. The dart generator can't produce a
   single enum class that covers all branches and the inline-enum
   template is suppressed for composed schemas. Fix: in
   AbstractDartCodegen.fromProperty, when the composed schema has 2+
   branches and default codegen guessed at isEnum=true, drop the enum
   projection (clear isEnum, enumName, _enum, allowableValues; reset
   datatypeWithEnum=dataType). Single-element composed schemas are
   unchanged.

2. Empty-property classes emitted invalid Dart syntax: `Foo({ })` (Dart
   rejects empty named-args), a dangling `&&` on `==`, and a missing
   rhs/`;` on `hashCode`. Fix: `{{#hasVars}}` guards in
   dart_constructor.mustache and native_class.mustache. Empty classes
   now emit `Foo()`, `==> identical(this, other) || other is Foo;`,
   and `hashCode => 0;`.

3. `Map<String, Map<String, T>>` properties were decoded with
   `mapCastOfType<String, dynamic>` (returns `Map<String, dynamic>`,
   doesn't cast inner generics) which can't be assigned to the declared
   field type. Fix: native_class.mustache's nested-map branch now emits
   `(json[r'X'] as Map).map((k, v) => MapEntry(k as String, (v as Map).cast<String, T>()))`.
   Also fixes the same broken pattern in the pre-existing
   additional_properties_class.dart and map_test.dart samples.

Verified red/green: pre-fix `dart analyze` reported the exact symptoms
(undefined StatusEnum, empty-class syntax errors, Map<String, dynamic>
not assignable). Post-fix all three model files are clean; remaining
errors in the fake sample are pre-existing and unrelated. Plain
petstore_client_lib sample analyzes clean. All 88 org.openapitools
.codegen.dart.* tests pass.

Closes OpenAPITools#23665, closes OpenAPITools#16715
Refs OpenAPITools#9272, OpenAPITools#12914, OpenAPITools#15670
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 76 files

Note: This PR contains a large number of files. cubic only reviews up to 75 files per PR, so some files may not have been reviewed. cubic prioritises the most important files to review.

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="samples/openapi3/client/petstore/dart2/petstore_client_lib_fake/lib/model/pet_reactions_response.dart">

<violation number="1" location="samples/openapi3/client/petstore/dart2/petstore_client_lib_fake/lib/model/pet_reactions_response.dart:16">
P2: Defaulting mutable map fields to `const {}` makes the zero-arg constructor return unmodifiable maps that throw on mutation.</violation>

<violation number="2" location="samples/openapi3/client/petstore/dart2/petstore_client_lib_fake/lib/model/pet_reactions_response.dart:33">
P2: Deep equality in `==` is not matched by `hashCode`, so equal instances can hash differently.</violation>
</file>

<file name="modules/openapi-generator/src/main/resources/dart2/serialization/native/native_class.mustache">

<violation number="1" location="modules/openapi-generator/src/main/resources/dart2/serialization/native/native_class.mustache:233">
P1: Required nested-map deserialization can still return null because `!` only applies to the else branch of the ternary, breaking non-nullable required fields.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(myReacts.hashCode) +
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Deep equality in == is not matched by hashCode, so equal instances can hash differently.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/openapi3/client/petstore/dart2/petstore_client_lib_fake/lib/model/pet_reactions_response.dart, line 33:

<comment>Deep equality in `==` is not matched by `hashCode`, so equal instances can hash differently.</comment>

<file context>
@@ -0,0 +1,117 @@
+  @override
+  int get hashCode =>
+    // ignore: unnecessary_parenthesis
+    (myReacts.hashCode) +
+      (reactionCounts.hashCode);
+  
</file context>

class PetReactionsResponse {
/// Returns a new [PetReactionsResponse] instance.
PetReactionsResponse({
this.myReacts = const {},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Defaulting mutable map fields to const {} makes the zero-arg constructor return unmodifiable maps that throw on mutation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/openapi3/client/petstore/dart2/petstore_client_lib_fake/lib/model/pet_reactions_response.dart, line 16:

<comment>Defaulting mutable map fields to `const {}` makes the zero-arg constructor return unmodifiable maps that throw on mutation.</comment>

<file context>
@@ -0,0 +1,117 @@
+class PetReactionsResponse {
+  /// Returns a new [PetReactionsResponse] instance.
+  PetReactionsResponse({
+    this.myReacts = const {},
+    this.reactionCounts = const {},
+  });
</file context>

…sion

Cubic review on OpenAPITools#23671 caught that the prior shape

    json[X] == null
        ? null
        : (json[X] as Map).map(...).cast<...>()!

applied the trailing ! only to the else branch, so a required
non-nullable Map<String, Map<...>> field could still be assigned null
from the ? branch.

Restructure to a single nullable expression that matches the existing
mapCastOfType pattern in the same file:

    (json[X] as Map?)?.map((k, v) => MapEntry(k as String, (v as Map).cast<String, T>()))
        {{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}
        {{^required}}{{#defaultValue}} ?? <default>{{/defaultValue}}{{/required}}

Now ! correctly applies to the whole nullable expression, ?? feeds the
default consistently with the other map branches, and the optional case
collapses to a single nullable expression instead of a ternary.
@winrid
Copy link
Copy Markdown
Contributor Author

winrid commented Apr 30, 2026

Thanks. Issue 1 (P1) is a real bug, fixed in 8df1ddd: restructured to (json[X] as Map?)?.map(...){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? <default>{{/defaultValue}}{{/required}} so ! applies to the whole nullable expression.

Issues 2 and 3 are pre-existing dart template behavior (#15763 tracks the ==/hashCode mismatch). Out of scope for this PR.

@cubic-dev-ai
Copy link
Copy Markdown
Contributor

cubic-dev-ai Bot commented Apr 30, 2026

Thanks @cubic-dev-ai. Took a look at the three:

Issue 1 (P1, native_class.mustache:233) is a real bug I introduced - confirmed. The ! was attached to the else-branch expression, so the ? branch could still return null and silently bind to a non-nullable required field. Fixed in 8df1ddd by restructuring to a single nullable expression that mirrors the mapCastOfType pattern already used in the same file:

...

@winrid That makes sense. The fix for Issue 1 in native_class.mustache is much cleaner now. Using the null-aware map access (json[...] as Map?)?.map(...) and then applying the bang operator or default value to the result of that entire expression correctly handles Dart's null safety for both required and optional fields. It matches the logic used in mapCastOfType and avoids the ternary operator pitfall.

Regarding Issues 2 and 3, I agree with keeping them out of scope here. Since the default collection initialization and the == vs hashCode inconsistency (tracked in #15763) are established patterns across the entire Dart generator, addressing them here would cause significant ripple effects in the samples beyond the specific bugs you're fixing.

You can manage how I learn from these types of project-specific conventions in your learnings or adjust my behavior in settings.

Added PetReactionStatus, PetReactionResponse, PetEmptyMetadata,
PetReactionsResponse to the dart fake-petstore yaml; that yaml is also
consumed by dart-dio configs. Regenerate to keep CI 'Samples
up-to-date' green.

Also strip the auto-generated test stub entries from the FILES index
because CI's regen filters them out (see OpenAPITools#23669 for the same pattern).
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 32 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/lib/src/model/pet_reaction_response.dart">

<violation number="1" location="samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/lib/src/model/pet_reaction_response.dart:88">
P2: Nullable field `petId` is deserialized with a non-null cast, so an explicit `null` value can crash deserialization.</violation>

<violation number="2" location="samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/lib/src/model/pet_reaction_response.dart:95">
P2: Nullable field `status` is deserialized with a non-null cast, so an explicit `null` value can crash deserialization.</violation>
</file>

<file name="samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake-json_serializable/lib/src/model/pet_reactions_response.dart">

<violation number="1" location="samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake-json_serializable/lib/src/model/pet_reactions_response.dart:55">
P2: `operator ==` compares nested `Map` fields by reference instead of deep value equality, so structurally equal responses can compare unequal.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

final valueDes = serializers.deserialize(
value,
specifiedType: const FullType(String),
) as String;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Nullable field status is deserialized with a non-null cast, so an explicit null value can crash deserialization.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/lib/src/model/pet_reaction_response.dart, line 95:

<comment>Nullable field `status` is deserialized with a non-null cast, so an explicit `null` value can crash deserialization.</comment>

<file context>
@@ -0,0 +1,126 @@
+          final valueDes = serializers.deserialize(
+            value,
+            specifiedType: const FullType(String),
+          ) as String;
+          result.status = valueDes;
+          break;
</file context>

final valueDes = serializers.deserialize(
value,
specifiedType: const FullType(int),
) as int;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Nullable field petId is deserialized with a non-null cast, so an explicit null value can crash deserialization.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake/lib/src/model/pet_reaction_response.dart, line 88:

<comment>Nullable field `petId` is deserialized with a non-null cast, so an explicit `null` value can crash deserialization.</comment>

<file context>
@@ -0,0 +1,126 @@
+          final valueDes = serializers.deserialize(
+            value,
+            specifiedType: const FullType(int),
+          ) as int;
+          result.petId = valueDes;
+          break;
</file context>



@override
bool operator ==(Object other) => identical(this, other) || other is PetReactionsResponse &&
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: operator == compares nested Map fields by reference instead of deep value equality, so structurally equal responses can compare unequal.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/openapi3/client/petstore/dart-dio/petstore_client_lib_fake-json_serializable/lib/src/model/pet_reactions_response.dart, line 55:

<comment>`operator ==` compares nested `Map` fields by reference instead of deep value equality, so structurally equal responses can compare unequal.</comment>

<file context>
@@ -0,0 +1,74 @@
+
+
+    @override
+    bool operator ==(Object other) => identical(this, other) || other is PetReactionsResponse &&
+      other.myReacts == myReacts &&
+      other.reactionCounts == reactionCounts;
</file context>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant