Skip to content

Commit 8b781ac

Browse files
authored
Improve error message when generating code for Record types with unsupported fields (#1561)
Fixes #1559
1 parent 6b711e3 commit 8b781ac

4 files changed

Lines changed: 201 additions & 26 deletions

File tree

json_serializable/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
## 6.13.1-wip
22

3+
- Improve error message when generating code for `Record` types with unsupported
4+
fields.
5+
([#1559](https://github.com/google/json_serializable.dart/issues/1559))
36
- Require `analyzer: ^10.0.0`
47
- Require `build: ^4.0.4`
58
- Require `dart_style: ^3.1.4`

json_serializable/lib/src/utils.dart

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -208,25 +208,46 @@ String encodedFieldName(FieldRename fieldRename, String declaredName) =>
208208
/// Return the Dart code presentation for the given [type].
209209
///
210210
/// This function is intentionally limited, and does not support all possible
211-
/// types and locations of these files in code. Specifically, it supports
212-
/// only [InterfaceType]s, with optional type arguments that are also should
213-
/// be [InterfaceType]s.
214-
String typeToCode(DartType type, {bool forceNullable = false}) {
215-
if (type is DynamicType) {
216-
return 'dynamic';
217-
} else if (type is InterfaceType) {
218-
return [
219-
type.element.name,
220-
if (type.typeArguments.isNotEmpty)
221-
'<${type.typeArguments.map(typeToCode).join(', ')}>',
222-
(type.isNullableType || forceNullable) ? '?' : '',
223-
].join();
224-
}
211+
/// types. Specifically, it supports [InterfaceType]s, [RecordType]s,
212+
/// [TypeParameterType]s, and [DynamicType]s.
213+
String typeToCode(DartType type, {bool forceNullable = false}) =>
214+
switch (type) {
215+
DynamicType() => 'dynamic',
216+
InterfaceType() => [
217+
type.element.name,
218+
if (type.typeArguments.isNotEmpty)
219+
'<${type.typeArguments.map(typeToCode).join(', ')}>',
220+
if (type.isNullableType || forceNullable) '?',
221+
].join(),
222+
TypeParameterType() => type.toStringNonNullable(),
223+
RecordType() => _recordTypeToCode(type, forceNullable),
224+
_ => type.getDisplayString(),
225+
};
225226

226-
if (type is TypeParameterType) {
227-
return type.toStringNonNullable();
227+
String _recordTypeToCode(RecordType type, bool forceNullable) {
228+
final positional = type.positionalFields
229+
.map((f) => typeToCode(f.type))
230+
.join(', ');
231+
final named = type.namedFields
232+
.map((f) => '${typeToCode(f.type)} ${f.name}')
233+
.join(', ');
234+
235+
final buffer = StringBuffer('(');
236+
if (positional.isNotEmpty) {
237+
buffer.write(positional);
238+
if (named.isNotEmpty) buffer.write(', ');
239+
}
240+
if (named.isNotEmpty) {
241+
buffer.write('{$named}');
242+
}
243+
if (type.positionalFields.length == 1 && type.namedFields.isEmpty) {
244+
buffer.write(',');
245+
}
246+
buffer.write(')');
247+
if (type.isNullableType || forceNullable) {
248+
buffer.write('?');
228249
}
229-
throw UnimplementedError('(${type.runtimeType}) $type');
250+
return buffer.toString();
230251
}
231252

232253
String? defaultDecodeLogic(

json_serializable/test/json_serializable_test.dart

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ Future<void> main() async {
5353
}
5454

5555
const _expectedAnnotatedTests = {
56+
'_BetterPrivateNames',
57+
'annotatedMethod',
5658
'BadEnumDefaultValue',
5759
'BadFromFuncReturnType',
5860
'BadNoArgs',
@@ -62,6 +64,7 @@ const _expectedAnnotatedTests = {
6264
'CtorDefaultValueAndJsonKeyDefaultValue',
6365
'CtorParamJsonKey',
6466
'CtorParamJsonKeyWithExtends',
67+
'DateTimeUtcTestClass',
6568
'DefaultDoubleConstants',
6669
'DefaultWithConstObject',
6770
'DefaultWithDisallowNullRequiredClass',
@@ -89,9 +92,9 @@ const _expectedAnnotatedTests = {
8992
'GenericClass',
9093
'IgnoreAndIncludeFromJsonFieldCtorClass',
9194
'IgnoreAndIncludeToJsonFieldCtorClass',
92-
'IgnoreUnannotated',
9395
'IgnoredFieldClass',
9496
'IgnoredFieldCtorClass',
97+
'IgnoreUnannotated',
9598
'IncludeIfNullDisallowNullClass',
9699
'IncludeIfNullOverride',
97100
'InvalidChildClassFromJson',
@@ -101,13 +104,14 @@ const _expectedAnnotatedTests = {
101104
'InvalidToFunc2Args',
102105
'Issue1038RegressionTest',
103106
'Issue713',
104-
'JsonConvertOnField',
105107
'JsonConverterCtorParams',
106108
'JsonConverterDuplicateAnnotations',
107109
'JsonConverterNamedCtor',
108110
'JsonConverterNullableToNonNullable',
109111
'JsonConverterOnGetter',
110112
'JsonConverterWithBadTypeArg',
113+
'JsonConvertOnField',
114+
'JsonSchemaTestClass',
111115
'JsonValueValid',
112116
'JsonValueWithBool',
113117
'JustSetter',
@@ -131,16 +135,24 @@ const _expectedAnnotatedTests = {
131135
'OverrideGetterExampleI613',
132136
'PrivateFieldCtorClass',
133137
'PropInMixinI448Regression',
138+
'RecordDoubleConverter',
139+
'RecordNamedDoubleConverter',
140+
'RecordNullableDoubleConverter',
141+
'RecordSingleDoubleConverter',
142+
'RecordWithFunction',
143+
'RecordWithNamedFunction',
144+
'RecordWithSinglePositionalFunction',
134145
'Reproduce869NullableGenericType',
135146
'Reproduce869NullableGenericTypeWithDefault',
136147
'SameCtorAndJsonKeyDefaultValue',
137148
'SetSupport',
149+
'SubclassedJsonKey',
138150
'SubType',
139151
'SubTypeWithAnnotatedFieldOverrideExtends',
140152
'SubTypeWithAnnotatedFieldOverrideExtendsWithOverrides',
141153
'SubTypeWithAnnotatedFieldOverrideImplements',
142-
'SubclassedJsonKey',
143154
'TearOffFromJsonClass',
155+
'theAnswer',
144156
'ToJsonNullableFalseIncludeIfNullFalse',
145157
'TypedConvertMethods',
146158
'UnknownEnumValue',
@@ -154,17 +166,13 @@ const _expectedAnnotatedTests = {
154166
'UnsupportedListField',
155167
'UnsupportedMapField',
156168
'UnsupportedNestedFunctionType',
169+
'UnsupportedMapKeyRecord',
157170
'UnsupportedSetField',
158171
'UnsupportedUriField',
159172
'ValidToFromFuncClassStatic',
160173
'WithANonCtorGetter',
161174
'WithANonCtorGetterChecked',
162175
'WrongConstructorNameClass',
163-
'_BetterPrivateNames',
164-
'annotatedMethod',
165-
'theAnswer',
166-
'JsonSchemaTestClass',
167-
'DateTimeUtcTestClass',
168176
};
169177

170178
const _expectedSchemaTests = {

json_serializable/test/src/_json_serializable_test_input.dart

Lines changed: 144 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -627,9 +627,152 @@ class DateTimeUtcTestClass {
627627
DateTimeUtcTestClass(this.date);
628628
}
629629

630+
@ShouldThrow(
631+
'Could not generate `fromJson` code for `record` because of type '
632+
'`void Function()`.',
633+
)
634+
@JsonSerializable()
635+
class RecordWithFunction {
636+
final (int, void Function()) record;
637+
638+
RecordWithFunction(this.record);
639+
}
640+
641+
@ShouldThrow(
642+
'Could not generate `fromJson` code for `record` because of type '
643+
'`void Function()`.',
644+
)
645+
@JsonSerializable()
646+
class RecordWithNamedFunction {
647+
final ({int a, void Function() b}) record;
648+
649+
RecordWithNamedFunction(this.record);
650+
}
651+
652+
@ShouldThrow(
653+
'Could not generate `fromJson` code for `record` because of type '
654+
'`void Function()`.',
655+
)
656+
@JsonSerializable()
657+
class RecordWithSinglePositionalFunction {
658+
final (void Function(),) record;
659+
660+
RecordWithSinglePositionalFunction(this.record);
661+
}
662+
663+
class RecordConverter1 extends JsonConverter<(int, int), Map<String, dynamic>> {
664+
const RecordConverter1();
665+
@override
666+
(int, int) fromJson(Map<String, dynamic> json) => (0, 0);
667+
@override
668+
Map<String, dynamic> toJson((int, int) object) => {};
669+
}
670+
671+
class RecordConverter2 extends JsonConverter<(int, int), Map<String, dynamic>> {
672+
const RecordConverter2();
673+
@override
674+
(int, int) fromJson(Map<String, dynamic> json) => (0, 0);
675+
@override
676+
Map<String, dynamic> toJson((int, int) object) => {};
677+
}
678+
679+
@ShouldThrow('Found more than one matching converter for `(int, int)`.')
680+
@JsonSerializable()
681+
@RecordConverter1()
682+
@RecordConverter2()
683+
class RecordDoubleConverter {
684+
late (int, int) record;
685+
}
686+
687+
class SingleRecordConverter1
688+
extends JsonConverter<(int,), Map<String, dynamic>> {
689+
const SingleRecordConverter1();
690+
@override
691+
(int,) fromJson(Map<String, dynamic> json) => (0,);
692+
@override
693+
Map<String, dynamic> toJson((int,) object) => {};
694+
}
695+
696+
class SingleRecordConverter2
697+
extends JsonConverter<(int,), Map<String, dynamic>> {
698+
const SingleRecordConverter2();
699+
@override
700+
(int,) fromJson(Map<String, dynamic> json) => (0,);
701+
@override
702+
Map<String, dynamic> toJson((int,) object) => {};
703+
}
704+
705+
@ShouldThrow('Found more than one matching converter for `(int,)`.')
706+
@JsonSerializable()
707+
@SingleRecordConverter1()
708+
@SingleRecordConverter2()
709+
class RecordSingleDoubleConverter {
710+
late (int,) record;
711+
}
712+
713+
class NullableRecordConverter1
714+
extends JsonConverter<(int, int)?, Map<String, dynamic>> {
715+
const NullableRecordConverter1();
716+
@override
717+
(int, int)? fromJson(Map<String, dynamic> json) => null;
718+
@override
719+
Map<String, dynamic> toJson((int, int)? object) => {};
720+
}
721+
722+
class NullableRecordConverter2
723+
extends JsonConverter<(int, int)?, Map<String, dynamic>> {
724+
const NullableRecordConverter2();
725+
@override
726+
(int, int)? fromJson(Map<String, dynamic> json) => null;
727+
@override
728+
Map<String, dynamic> toJson((int, int)? object) => {};
729+
}
730+
731+
@ShouldThrow('Found more than one matching converter for `(int, int)?`.')
732+
@JsonSerializable()
733+
@NullableRecordConverter1()
734+
@NullableRecordConverter2()
735+
class RecordNullableDoubleConverter {
736+
late (int, int)? record;
737+
}
738+
739+
class NamedRecordConverter1
740+
extends JsonConverter<({int a, int b}), Map<String, dynamic>> {
741+
const NamedRecordConverter1();
742+
@override
743+
({int a, int b}) fromJson(Map<String, dynamic> json) => (a: 0, b: 0);
744+
@override
745+
Map<String, dynamic> toJson(({int a, int b}) object) => {};
746+
}
747+
748+
class NamedRecordConverter2
749+
extends JsonConverter<({int a, int b}), Map<String, dynamic>> {
750+
const NamedRecordConverter2();
751+
@override
752+
({int a, int b}) fromJson(Map<String, dynamic> json) => (a: 0, b: 0);
753+
@override
754+
Map<String, dynamic> toJson(({int a, int b}) object) => {};
755+
}
756+
757+
@ShouldThrow('Found more than one matching converter for `({int a, int b})`.')
758+
@JsonSerializable()
759+
@NamedRecordConverter1()
760+
@NamedRecordConverter2()
761+
class RecordNamedDoubleConverter {
762+
late ({int a, int b}) record;
763+
}
764+
630765
@ShouldThrow('''
631-
Could not generate `fromJson` code for `field` because type is unimplemented (UnimplementedError: (FunctionTypeImpl) void Function()).''')
766+
Could not generate `fromJson` code for `field` because of type `void Function()`.''')
632767
@JsonSerializable(createToJson: false)
633768
class UnsupportedNestedFunctionType {
634769
late List<void Function()> field;
635770
}
771+
772+
@ShouldThrow('''
773+
Could not generate `fromJson` code for `map` because of type `(int, int)`.
774+
Map keys must be one of: Object, dynamic, enum, String, BigInt, DateTime, int, Uri.''')
775+
@JsonSerializable(createToJson: false)
776+
class UnsupportedMapKeyRecord {
777+
late Map<(int, int), String> map;
778+
}

0 commit comments

Comments
 (0)