[dart-dio][built_value] Honor optional non-nullable properties in deserialize_properties.mustache#23661
Open
Homegan wants to merge 2 commits intoOpenAPITools:masterfrom
Conversation
`class_members.mustache` makes the Dart getter `String?` whenever `isNullable || !required` (the only sane Dart mapping: an optional field can always be observably `null`), but `deserialize_properties.mustache` only honored `isNullable`. The two templates therefore disagreed: getter was `String?` but the deserializer cast the value as non-nullable `String`, throwing `type 'Null' is not a subtype of type 'String'` the moment the API returned the field as `null`. The throw bubbled up through any enclosing container, so a single null leaf could tank the entire parent payload -- and most call paths swallowed the error silently. Fix: in `deserialize_properties.mustache` the cast and the FullType now key on the same condition as the getter: nullable when `isNullable || !required`. The null-skip guard (`if (valueDes == null) continue;`) is also extended to optional non-nullable properties so we never reach the builder assignment with a null on the wire. Required + non-nullable, required + explicitly nullable, and optional + explicitly nullable all keep their existing behavior -- only the previously-broken optional non-nullable path changes. A new fixture `built_value_optional_nullable.yaml` exercises every shape, and a new test `DartDioClientCodegenTest.testOptionalNonNullablePropertyDeserializesAsNullable` asserts the generated `watch_provider_entry.dart` contains the expected lines for each. Full Dart suite: 115 tests, 0 failures, 0 regressions.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
[dart-dio][built_value]Honor optional non-nullable properties indeserialize_properties.mustachePR checklist
[dart-dio][built_value].master.Summary
The dart-dio built_value generator emits a Dart getter typed
String?(nullable) for any property that is optional in OpenAPI — even when the schema doesn't saynullable: true. This is the only sane Dart mapping: an absent JSON key is observablynullin Dart code.But the matching deserializer in
deserialize_properties.mustacheonly honored OpenAPInullable: trueand emitted a non-nullableFullType(String)+ castas String. The two templates therefore disagree, and the cast throwstype 'Null' is not a subtype of type 'String'the moment the API returns the field asnull.Because the throw bubbles up through every enclosing container, a single null leaf can tank the entire parent payload — and most call paths swallow the error silently.
Reproduction
Generated
widget.dartbefore this PR:Send
{ "iconUrl": null }and the cast throws.Root cause
class_members.mustachealready emits a nullable getter when the Dart side has to admitnull:i.e. nullable iff
isNullable || !required. Butdeserialize_properties.mustacheonly keys onisNullable. So the getter isString?and the deserializer cast isas String— internally inconsistent.Fix
In
deserialize_properties.mustache, the cast and theFullTypenow follow the same condition as the getter, and the existing null-skip guard is extended to the new branch:After this PR:
FullType(X)/as XFullType(X)/as Xnullable: trueFullType.nullable(X)/as X?+ skip-on-nullFullType(X)/as X(BUG)FullType.nullable(X)/as X?+ skip-on-nullnullable: trueFullType.nullable(X)/as X?+ skip-on-nullOnly the third row — the previously broken case — changes.
Tests
A new fixture
src/test/resources/3_0/dart-dio/built_value_optional_nullable.yamlcovers all four rows of the table on a minimalWidgetschema (id,namerequired;iconUrl,priorityoptional non-nullable;explicitlyNullableoptional +nullable: true). A new testDartDioClientCodegenTest.testOptionalNonNullablePropertyDeserializesAsNullableasserts the generatedwidget.dartcontains the expected lines for each.Files changed
3 files, +131 / −2.
Compatibility
nullon the wire is now correctly accepted and skipped.isNullableandrequired, not on the spec dialect.Side effect, intentional
Models that used to silently lose data (when a parent payload contained any null leaf on an optional non-nullable field) will start surfacing the value correctly. If a downstream consumer was relying on the field being missing rather than null, they'll now see null. This is the correct observable behavior — the previous behavior was the throw + a silent catch-and-drop somewhere up the stack.
Related
There's a separate, independent bug in the dart-dio built_value generator where types reachable only through a model property's
additionalPropertiesnever get aBuilderFactoryregistered (Bad state: No builder factory for BuiltList<X>). I'm filing it as a separate PR — the two fixes touch different files and have different risk profiles, and either can land without the other.Summary by cubic
Fixes deserialization of optional non-nullable properties in the
dart-dio+built_valuegenerator by accepting nulls on the wire and skipping null assignments to prevent cast crashes. Regenerates the petstore sample and adds a regression test + fixture to cover this behavior.FullType.nullable(...)and castas T?whenisNullable || !requiredindeserialize_properties.mustache.valueDes == nullfor optional fields.petstore_client_lib_fakeand addedbuilt_value_optional_nullable.yaml+DartDioClientCodegenTest.testOptionalNonNullablePropertyDeserializesAsNullable.Written for commit 63babf6. Summary will update on new commits. Review in cubic