Skip to content

Commit b96c33d

Browse files
committed
Allow nulls if a generic type argument allows nulls.
1 parent 9fa59d5 commit b96c33d

File tree

9 files changed

+105
-35
lines changed

9 files changed

+105
-35
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
# 8.10.0
44

5+
- Allow `null` values in generic types when the type argument is nullable. For
6+
example, if a class `Value<T>` has a field of type `T`, a `Value<int?>` can
7+
now hold a `null` value.
58
- Stop generating unnecessary `new` keywords.
69
- Stop generating explicit null checks in constructors: these are not needed
710
with sound null safety.

built_value_generator/lib/src/serializer_source_class.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,7 @@ class $serializerImplName implements PrimitiveSerializer<$genericName> {
475475

476476
String _generateRequiredFieldSerializers() {
477477
return fields
478-
.where((field) => !field.isNullable)
478+
.where((field) => !field.isNullable && !field.hasGenericType)
479479
.map((field) => "'${escapeString(field.wireName)}', "
480480
'serializers.serialize(object.${field.name}, '
481481
'specifiedType: '
@@ -484,7 +484,7 @@ class $serializerImplName implements PrimitiveSerializer<$genericName> {
484484
}
485485

486486
String _generateNullableFieldSerializers() {
487-
var nullableFields = fields.where((field) => field.isNullable).toList();
487+
var nullableFields = fields.where((field) => field.isNullable || field.hasGenericType).toList();
488488
if (nullableFields.isEmpty) return '';
489489

490490
return 'Object? value;' +

built_value_generator/lib/src/serializer_source_field.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ abstract class SerializerSourceField
8686
element.getter?.returnType.nullabilitySuffix ==
8787
NullabilitySuffix.question;
8888

89+
@memoized
90+
bool get hasGenericType =>
91+
element.getter?.returnType is TypeParameterType;
92+
8993
@memoized
9094
bool get isNullable => hasNullableAnnotation || hasNullableType;
9195

built_value_generator/lib/src/value_source_class.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1048,7 +1048,7 @@ abstract class ValueSourceClass
10481048
if (!field.isNullable) {
10491049
needsNullCheck.add(name);
10501050
}
1051-
if (field.hasNullableGenericType) {
1051+
if (field.hasGenericType) {
10521052
genericFields[name] =
10531053
field.element.getter!.returnType.element!.displayName;
10541054
}

built_value_generator/lib/src/value_source_field.dart

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -109,12 +109,8 @@ abstract class ValueSourceField
109109
NullabilitySuffix.question;
110110

111111
@memoized
112-
bool get hasNullableGenericType =>
113-
element.getter?.returnType is TypeParameterType &&
114-
(element.getter!.returnType as TypeParameterType)
115-
.bound
116-
.nullabilitySuffix ==
117-
NullabilitySuffix.question;
112+
bool get hasGenericType =>
113+
element.getter?.returnType is TypeParameterType;
118114

119115
@memoized
120116
bool get isNullable => hasNullableAnnotation || hasNullableType;

built_value_generator/lib/src/value_source_field.g.dart

Lines changed: 0 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

end_to_end_test/lib/generics.g.dart

Lines changed: 38 additions & 21 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

end_to_end_test/test/generics_serializer_test.dart

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,37 @@ void main() {
6767
});
6868
});
6969

70+
group('GenericValue with known specifiedType, correct builder and null', () {
71+
var data = GenericValue<int?>();
72+
var specifiedType = const FullType(GenericValue, [FullType.nullable(int)]);
73+
var serializersWithBuilder = (serializers.toBuilder()
74+
..addBuilderFactory(specifiedType, () => GenericValueBuilder<int?>()))
75+
.build();
76+
var serialized = json.decode(json.encode([])) as Object;
77+
78+
test('can be serialized', () {
79+
expect(
80+
serializersWithBuilder.serialize(data, specifiedType: specifiedType),
81+
serialized);
82+
});
83+
84+
test('can be deserialized', () {
85+
expect(
86+
serializersWithBuilder.deserialize(serialized,
87+
specifiedType: specifiedType),
88+
data);
89+
});
90+
91+
test('keeps generic type on deserialization', () {
92+
expect(
93+
serializersWithBuilder
94+
.deserialize(serialized, specifiedType: specifiedType)
95+
.runtimeType
96+
.toString(),
97+
r'_$GenericValue<int?>');
98+
});
99+
});
100+
70101
group('GenericValue with unknown specifiedType', () {
71102
var data = GenericValue<int>((b) => b..value = 1);
72103
var serialized = json.decode(json.encode([
@@ -89,6 +120,26 @@ void main() {
89120
});
90121
});
91122

123+
group('GenericValue with unknown specifiedType null value', () {
124+
var data = GenericValue<int?>();
125+
var serialized = json.decode(json.encode([
126+
'GenericValue',
127+
])) as Object;
128+
129+
test('can be serialized', () {
130+
expect(serializers.serialize(data), serialized);
131+
});
132+
133+
test('can be deserialized', () {
134+
expect(serializers.deserialize(serialized), data);
135+
});
136+
137+
test('loses generic type on deserialization', () {
138+
expect(serializers.deserialize(serialized).runtimeType.toString(),
139+
r'_$GenericValue<Object?>');
140+
});
141+
});
142+
92143
group('BoundGenericValue with known specifiedType but missing builder', () {
93144
var data = BoundGenericValue<int>((b) => b..value = 1);
94145
var specifiedType = const FullType(BoundGenericValue, [FullType(int)]);

end_to_end_test/test/generics_test.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ void main() {
1818
throwsA(const TypeMatcher<BuiltValueNullFieldError>()));
1919
});
2020

21+
test('does not throw on null for nullable fields on build', () {
22+
GenericValue<int?>();
23+
});
24+
2125
test('fields can be set via build constructor', () {
2226
final value = GenericValue<int>((b) => b..value = 1);
2327
expect(value.value, 1);

0 commit comments

Comments
 (0)