Skip to content

Commit be8e9a3

Browse files
authored
Support Dart 3 Record types (#1314)
Closes #1222
1 parent 8c39dd2 commit be8e9a3

26 files changed

+2400
-26
lines changed

json_serializable/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
## 6.7.0-dev
1+
## 6.7.0
22

3+
- Support `Record` types.
34
- Require Dart 3.0
45
- Require `analyzer: ^5.12.0`
56

json_serializable/README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,11 +152,11 @@ Out of the box, `json_serializable` supports many common types in the
152152
[dart:core](https://api.dart.dev/stable/dart-core/dart-core-library.html)
153153
library:
154154
[`BigInt`], [`bool`], [`DateTime`], [`double`], [`Duration`], [`Enum`], [`int`],
155-
[`Iterable`], [`List`], [`Map`], [`num`], [`Object`], [`Set`], [`String`],
156-
[`Uri`]
155+
[`Iterable`], [`List`], [`Map`], [`num`], [`Object`], [`Record`], [`Set`],
156+
[`String`], [`Uri`]
157157

158158
The collection types –
159-
[`Iterable`], [`List`], [`Map`], [`Set`]
159+
[`Iterable`], [`List`], [`Map`], [`Record`], [`Set`]
160160
– can contain values of all the above types.
161161

162162
For [`Map`], the key value must be one of
@@ -313,6 +313,7 @@ targets:
313313
[`Map`]: https://api.dart.dev/stable/dart-core/Map-class.html
314314
[`num`]: https://api.dart.dev/stable/dart-core/num-class.html
315315
[`Object`]: https://api.dart.dev/stable/dart-core/Object-class.html
316+
[`Record`]: https://api.dart.dev/stable/dart-core/Record-class.html
316317
[`Set`]: https://api.dart.dev/stable/dart-core/Set-class.html
317318
[`String`]: https://api.dart.dev/stable/dart-core/String-class.html
318319
[`Uri`]: https://api.dart.dev/stable/dart-core/Uri-class.html

json_serializable/build.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ builders:
7171
- .type_map.dart
7272
- .type_num.dart
7373
- .type_object.dart
74+
- .type_record.dart
7475
- .type_set.dart
7576
- .type_string.dart
7677
- .type_uri.dart

json_serializable/lib/src/enum_utils.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ Map<FieldElement, Object?>? _enumMap(
5252
DartType targetType, {
5353
bool nullWithNoAnnotation = false,
5454
}) {
55-
final annotation = _jsonEnumChecker.firstAnnotationOf(targetType.element!);
55+
final targetTypeElement = targetType.element;
56+
if (targetTypeElement == null) return null;
57+
final annotation = _jsonEnumChecker.firstAnnotationOf(targetTypeElement);
5658
final jsonEnum = _fromAnnotation(annotation);
5759

5860
final enumFields = iterateEnumFields(targetType);

json_serializable/lib/src/helper_core.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,9 @@ $converterOrKeyInstructions
7777
} else if (field.type != error.type) {
7878
message = '$message because of type `${typeToCode(error.type)}`';
7979
} else {
80+
final element = error.type.element?.name;
8081
todo = '''
81-
To support the type `${error.type.element!.name}` you can:
82+
To support the type `${element ?? error.type}` you can:
8283
$converterOrKeyInstructions''';
8384
}
8485

json_serializable/lib/src/json_key_utils.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ KeyConfig _from(FieldElement element, ClassConfig classAnnotation) {
6464
// literal, which is NOT supported!
6565
badType = 'Function';
6666
} else if (!reader.isLiteral) {
67-
badType = dartObject.type!.element!.name;
67+
badType = dartObject.type!.element?.name;
6868
}
6969

7070
if (badType != null) {

json_serializable/lib/src/settings.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import 'type_helpers/iterable_helper.dart';
1616
import 'type_helpers/json_converter_helper.dart';
1717
import 'type_helpers/json_helper.dart';
1818
import 'type_helpers/map_helper.dart';
19+
import 'type_helpers/record_helper.dart';
1920
import 'type_helpers/uri_helper.dart';
2021
import 'type_helpers/value_helper.dart';
2122

@@ -24,6 +25,7 @@ class Settings {
2425
IterableHelper(),
2526
MapHelper(),
2627
EnumHelper(),
28+
RecordHelper(),
2729
ValueHelper(),
2830
];
2931

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import 'package:analyzer/dart/element/type.dart';
2+
import 'package:source_helper/source_helper.dart';
3+
4+
import '../type_helper.dart';
5+
import '../utils.dart';
6+
7+
class RecordHelper extends TypeHelper<TypeHelperContextWithConfig> {
8+
const RecordHelper();
9+
10+
@override
11+
Object? deserialize(
12+
DartType targetType,
13+
String expression,
14+
TypeHelperContextWithConfig context,
15+
bool defaultProvided,
16+
) {
17+
if (targetType is! RecordType) return null;
18+
19+
final items = <Object>[];
20+
21+
const paramName = r'$jsonValue';
22+
23+
var index = 1;
24+
for (var field in targetType.positionalFields) {
25+
final indexer = escapeDartString('\$$index');
26+
items.add(
27+
context.deserialize(field.type, '$paramName[$indexer]')!,
28+
);
29+
index++;
30+
}
31+
for (var field in targetType.namedFields) {
32+
final indexer = escapeDartString(field.name);
33+
items.add(
34+
'${field.name}:'
35+
'${context.deserialize(field.type, '$paramName[$indexer]')!}',
36+
);
37+
}
38+
39+
if (items.isEmpty) {
40+
return '()';
41+
}
42+
43+
context.addMember(
44+
_recordConvertImpl(
45+
nullable: targetType.isNullableType, anyMap: context.config.anyMap),
46+
);
47+
48+
final recordLiteral = '(${items.map((e) => '$e,').join()})';
49+
50+
final helperName = _recordConvertName(
51+
nullable: targetType.isNullableType,
52+
anyMap: context.config.anyMap,
53+
);
54+
55+
return '''
56+
$helperName(
57+
$expression,
58+
($paramName) => $recordLiteral,
59+
)''';
60+
}
61+
62+
@override
63+
Object? serialize(
64+
DartType targetType,
65+
String expression,
66+
TypeHelperContextWithConfig context,
67+
) {
68+
if (targetType is! RecordType) return null;
69+
70+
final maybeBang = targetType.isNullableType ? '!' : '';
71+
72+
final items = <Object>[];
73+
74+
var index = 1;
75+
for (var field in targetType.positionalFields) {
76+
final indexer = escapeDartString('\$$index');
77+
items.add(
78+
'$indexer:'
79+
'${context.serialize(field.type, '$expression$maybeBang.\$$index')!}',
80+
);
81+
index++;
82+
}
83+
for (var field in targetType.namedFields) {
84+
final indexer = escapeDartString(field.name);
85+
items.add(
86+
'$indexer:${context.serialize(
87+
field.type,
88+
'$expression$maybeBang.${field.name}',
89+
)!}',
90+
);
91+
}
92+
93+
final mapValue = '{${items.map((e) => '$e,').join()}}';
94+
95+
return targetType.isNullableType
96+
? ifNullOrElse(
97+
expression,
98+
'null',
99+
mapValue,
100+
)
101+
: mapValue;
102+
}
103+
}
104+
105+
String _recordConvertName({required bool nullable, required bool anyMap}) =>
106+
'_\$recordConvert${anyMap ? 'Any' : ''}${nullable ? 'Nullable' : ''}';
107+
108+
String _recordConvertImpl({required bool nullable, required bool anyMap}) {
109+
final name = _recordConvertName(nullable: nullable, anyMap: anyMap);
110+
111+
var expression =
112+
'convert(value as ${anyMap ? 'Map' : 'Map<String, dynamic>'})';
113+
if (nullable) {
114+
expression = ifNullOrElse('value', 'null', expression);
115+
}
116+
117+
return '''
118+
\$Rec${nullable ? '?' : ''} $name<\$Rec>(
119+
Object? value,
120+
\$Rec Function(Map) convert,
121+
) =>
122+
$expression;
123+
''';
124+
}

json_serializable/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: json_serializable
2-
version: 6.7.0-dev
2+
version: 6.7.0
33
description: >-
44
Automatically generate code for converting to and from JSON by annotating
55
Dart classes.

json_serializable/test/kitchen_sink/kitchen_sink.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,8 @@ class KitchenSink implements k.KitchenSink {
184184
_validatedPropertyNo42 = value;
185185
}
186186

187+
k.RecordSample? recordField;
188+
187189
bool operator ==(Object other) => k.sinkEquals(this, other);
188190

189191
static Object? _trickyValueAccessor(Map json, String key) {

0 commit comments

Comments
 (0)