Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,16 @@
case r'{{baseName}}':
final valueDes = serializers.deserialize(
value,
specifiedType: const {{>serialization/built_value/variable_serializer_type}},
) as {{>serialization/built_value/variable_type}};
specifiedType: const FullType{{#isNullable}}.nullable{{/isNullable}}{{^isNullable}}{{^required}}.nullable{{/required}}{{/isNullable}}({{#isContainer}}{{baseType}}, [{{#isMap}}FullType(String), {{/isMap}}{{#items}}{{>serialization/built_value/variable_serializer_type}}{{/items}}]{{/isContainer}}{{^isContainer}}{{{datatypeWithEnum}}}{{/isContainer}}),
) as {{#isContainer}}{{baseType}}<{{#isMap}}String, {{/isMap}}{{#items}}{{>serialization/built_value/variable_type}}{{/items}}>{{/isContainer}}{{^isContainer}}{{{datatypeWithEnum}}}{{/isContainer}}{{#isNullable}}?{{/isNullable}}{{^isNullable}}{{^required}}?{{/required}}{{/isNullable}};
{{#isNullable}}
if (valueDes == null) continue;
{{/isNullable}}
{{^isNullable}}
{{^required}}
if (valueDes == null) continue;
{{/required}}
{{/isNullable}}
{{#isContainer}}
result.{{{name}}}.replace(valueDes);
{{/isContainer}}
Expand Down Expand Up @@ -54,4 +59,4 @@
break;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,79 @@ public void testImportMappingsInSerializersAndBarrelFile() throws IOException {
"package:my_api/src/model/custom_address.dart");
}

/**
* Regression test for the dart-dio built_value generator's handling
* of optional non-nullable model properties.
*
* Before the fix, the generator emitted a Dart getter typed as
* {@code String?} (because in Dart, an optional field can always be
* absent, which is observably {@code null}) but the matching
* deserializer used {@code FullType(String)} and cast the result
* {@code as String}. As a consequence, the moment the JSON payload
* carried the field as an explicit {@code null} the cast threw and
* the entire enclosing object failed to deserialize -- silently in
* many call paths.
*
* The fix: in {@code deserialize_properties.mustache} the cast
* follows the same rule that {@code class_members.mustache} already
* uses for the getter type, i.e. nullable when
* {@code isNullable || !required}. This test asserts that.
*/
@Test
public void testOptionalNonNullablePropertyDeserializesAsNullable() throws IOException {
File output = Files.createTempDirectory("test").toFile();
output.deleteOnExit();

final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("dart-dio")
.setInputSpec("src/test/resources/3_0/dart-dio/built_value_optional_nullable.yaml")
.setOutputDir(output.getAbsolutePath().replace("\\", "/"));

ClientOptInput opts = configurator.toClientOptInput();
Generator generator = new DefaultGenerator().opts(opts);
List<File> files = generator.generate();
files.forEach(File::deleteOnExit);

Path widget = output.toPath().resolve("lib/src/model/widget.dart");

// Required fields stay strict: cast as a non-nullable type with a
// non-nullable FullType. Otherwise the existing behaviour for
// required fields would change.
TestUtils.assertFileContains(widget,
"case r'id':",
"specifiedType: const FullType(int),",
"as int;");
TestUtils.assertFileContains(widget,
"case r'name':",
"specifiedType: const FullType(String),",
"as String;");

// Optional non-nullable: getter is `String?` (existing), and the
// deserializer now matches -- FullType.nullable + cast as `T?`
// + skip-on-null guard so we never reach `result.x = valueDes`
// with a null.
TestUtils.assertFileContains(widget, "String? get iconUrl;");
TestUtils.assertFileContains(widget,
"case r'iconUrl':",
"specifiedType: const FullType.nullable(String),",
"as String?;",
"if (valueDes == null) continue;");

TestUtils.assertFileContains(widget, "int? get priority;");
TestUtils.assertFileContains(widget,
"case r'priority':",
"specifiedType: const FullType.nullable(int),",
"as int?;",
"if (valueDes == null) continue;");

// Explicitly nullable still works (regression guard).
TestUtils.assertFileContains(widget,
"case r'explicitlyNullable':",
"specifiedType: const FullType.nullable(String),",
"as String?;",
"if (valueDes == null) continue;");
}

@Test
public void verifyDartDioGeneratorRuns() throws IOException {
File output = Files.createTempDirectory("test").toFile();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
openapi: "3.0.0"
info:
title: Built-value optional / nullable property test
version: "1.0.0"
paths:
/widget/{id}:
get:
operationId: getWidget
parameters:
- name: id
in: path
required: true
schema: { type: integer }
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/Widget"
components:
schemas:
Widget:
type: object
required:
- id
- name
properties:
# required + non-nullable: deserialize as `int`.
id:
type: integer
# required + non-nullable: deserialize as `String`.
name:
type: string
# optional + non-nullable in OpenAPI: dart getter is `String?`
# because Dart can't distinguish "missing" from "null". The
# deserializer must therefore ALSO accept null and skip the
# assignment, otherwise the cast `as String` throws when the
# API returns the field as null.
iconUrl:
type: string
# optional + non-nullable, integer flavor: same shape as iconUrl
# but exercises FullType(int) instead of FullType(String).
priority:
type: integer
# optional + explicitly nullable: existing behavior, kept as a
# regression guard so the fix doesn't break it.
explicitlyNullable:
type: string
nullable: true
Loading