Skip to content

[BUG][DART] PATCH tri-state optional handling - #23696

Merged
wing328 merged 4 commits into
OpenAPITools:masterfrom
radelcom:radelcom-fix/dart-patch-tri-state
Jul 31, 2026
Merged

[BUG][DART] PATCH tri-state optional handling#23696
wing328 merged 4 commits into
OpenAPITools:masterfrom
radelcom:radelcom-fix/dart-patch-tri-state

Conversation

@radelcom

@radelcom radelcom commented May 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds minimal PATCH tri-state handling for dart-dio with built_value serialization.

This fixes the PATCH case where the generator needs to preserve three distinct states for nullable fields:

  • absent
  • present with null
  • present with a value

The PR is intentionally scoped to the serialization path only to keep review risk low.

Changes

  • mark models with wrapped optional PATCH properties using x-has-optional-properties
  • export optional.dart when useOptional=true
  • import optional.dart in generated built_value models when needed
  • initialize wrapped fields with Optional.absent()
  • serialize wrapped fields only when object.field.isPresent
  • emit null when the present wrapped value is null
  • use unwrapped datatype metadata for serializer typing

Validation

Validated locally with:

./mvnw clean package -DskipTests

Build result: success.

Also validated by generating a Dart Dio sample from the built CLI jar. Generation completed successfully.

./mvnw clean package with tests enabled hit unrelated existing Kotlin Spring test failures on the local Java 25 environment, so this PR was validated through successful package build and Dart generation behavior.

How to validate

  1. Build the project:
./mvnw clean package -DskipTests
  1. Generate a Dart Dio client.
  2. Confirm wrapped PATCH fields:
  • default to Optional.absent()
  • are omitted when absent
  • serialize as key: null for Optional.present(null)
  • serialize as key: value for Optional.present(value)

/cc @wing328 @jaumard


Summary by cubic

Fixes PATCH tri-state handling in dart-dio with built_value, covering both serialization and deserialization so optional fields support absent, present-null, and present-value without mis-serialization.

  • Bug Fixes
    • Wrap optional PATCH fields in Optional and default to Optional.absent(); keep the outer Optional non-null (Optional<T?>, not Optional<T?>?).
    • Serialization: emit keys only when isPresent; write null for present-with-null.
    • Deserialization: set Optional.present(value) (including null), keep Optional.absent() when the key is missing; use unwrapped FullType metadata via x-unwrapped-datatype/x-unwrapped-datatype-nullable.
    • Wire-up: export optional.dart when useOptional=true and import it only for models with x-has-optional-properties; avoid whitespace-only template churn.

Written for commit 70b3b61. Summary will update on new commits.

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

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.

No issues found across 6 files

@wing328

wing328 commented May 5, 2026

Copy link
Copy Markdown
Member

cc @jaumard (2018/09) @josh-burton (2019/12) @amondnet (2019/12) @sbu-WBT (2020/12) @kuhnroyal (2020/12) @agilob (2020/12) @ahmednfwela (2021/08)

radelcom added 2 commits May 6, 2026 08:07
The outer Optional should never be null—only the inner value can be null.
Field type should be Optional<T?>, not Optional<T?>?

This fixes compilation errors when serializer tries to access .isPresent
on a potentially null Optional.
@pbalduino

pbalduino commented Jun 29, 2026

Copy link
Copy Markdown

Just want to add an independent real-world repro that confirms this fix is needed and validates the root cause.

We generate a dart-dio client (with useOptional: true and serializationLibrary: built_value) from our own OpenAPI contract. After regenerating against a spec change that added optional fields to a response model, deserializing any real JSON response through standardSerializers.deserialize(...) started failing at runtime with:

Bad state: No serializer for 'Optional<String?>'.

This isn't limited to one model. It affects every generated model that has at least one optional field, since serializers.g.dart never registers a built_value Serializer for the Optional<T> wrapper type, even though the models themselves reference it.

Minimal repro against a generated client:

final result = standardSerializers.deserialize(
  jsonDecode(jsonResponseBody),
  specifiedType: const FullType(SomeGeneratedResponseModel),
);
// Bad state: No serializer for 'Optional<String?>'.

Looking at the diff here, variable_serializer_type.mustache resolving the serializer from the unwrapped datatype instead of Optional<T> itself, plus the new class_serializer.mustache / deserialize_properties.mustache logic, looks like exactly the fix needed for the built_value path. Happy to test this against our use case once it's ready for review/testing. Let me know if a manual patch build would help validate it faster.

@wing328 wing328 added this to the 7.25.0 milestone Jul 31, 2026
@wing328
wing328 merged commit 4aed9c1 into OpenAPITools:master Jul 31, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants