Skip to content

Commit f4407b0

Browse files
committed
feat: throw on invalid config and add support for complex sealed classes
1 parent 1fc5285 commit f4407b0

File tree

4 files changed

+314
-7
lines changed

4 files changed

+314
-7
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import 'package:json_annotation/json_annotation.dart';
2+
3+
part 'complex_sealed_class_examples.g.dart';
4+
5+
@JsonSerializable(
6+
unionDiscriminator: 'base_base_base_type',
7+
)
8+
sealed class BaseBaseBaseType {
9+
BaseBaseBaseType();
10+
11+
factory BaseBaseBaseType.fromJson(Map<String, dynamic> json) =>
12+
_$BaseBaseBaseTypeFromJson(json);
13+
14+
Map<String, dynamic> toJson() => _$BaseBaseBaseTypeToJson(this);
15+
}
16+
17+
@JsonSerializable(
18+
unionDiscriminator: 'base_base_type',
19+
)
20+
sealed class BaseBase extends BaseBaseBaseType {
21+
BaseBase();
22+
}
23+
24+
@JsonSerializable(
25+
unionDiscriminator: 'base_type',
26+
)
27+
sealed class Base extends BaseBase {
28+
Base();
29+
}
30+
31+
@JsonSerializable()
32+
class FirstBaseImpl extends Base {
33+
FirstBaseImpl(this.value);
34+
35+
String value;
36+
}
37+
38+
@JsonSerializable()
39+
class SecondBaseImpl extends Base {
40+
SecondBaseImpl(this.value);
41+
42+
String value;
43+
}
44+
45+
@JsonSerializable()
46+
class BaseBaseImpl extends BaseBase {
47+
BaseBaseImpl(this.value);
48+
49+
String value;
50+
}
51+
52+
@JsonSerializable(
53+
createToJson: false,
54+
)
55+
sealed class SecondBase {
56+
SecondBase();
57+
58+
factory SecondBase.fromJson(Map<String, dynamic> json) =>
59+
_$SecondBaseFromJson(json);
60+
}
61+
62+
@JsonSerializable(
63+
createToJson: false,
64+
)
65+
sealed class ThirdBase {
66+
ThirdBase();
67+
68+
factory ThirdBase.fromJson(Map<String, dynamic> json) =>
69+
_$ThirdBaseFromJson(json);
70+
}
71+
72+
@JsonSerializable(
73+
createToJson: false,
74+
)
75+
class ImplAll implements SecondBase, ThirdBase {
76+
ImplAll(this.value);
77+
78+
String value;
79+
}

example/lib/complex_sealed_class_examples.g.dart

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

json_serializable/lib/src/generator_helper.dart

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,55 @@ class GeneratorHelper extends HelperCore with EncodeHelper, DecodeHelper {
5151
);
5252
}
5353

54+
final sealedSuperClassesOrEmpty = sealedSuperClasses(element);
55+
56+
final sealedDiscriminators = sealedSuperClassesOrEmpty
57+
.map((sealedClass) => jsonSerializableConfig(sealedClass, _generator))
58+
.map((config) => config?.unionDiscriminator);
59+
60+
if ((sealedSuperClassesOrEmpty.isNotEmpty || element.isSealed) &&
61+
config.genericArgumentFactories) {
62+
throw InvalidGenerationSourceError(
63+
'The class `${element.displayName}` is annotated '
64+
'with `JsonSerializable` field `genericArgumentFactories: true`. '
65+
'`genericArgumentFactories: true` is not supported for classes '
66+
'that are sealed or have sealed superclasses.',
67+
todo: 'Remove the `genericArgumentFactories` option or '
68+
'remove the `sealed` keyword from the class.',
69+
element: element,
70+
);
71+
}
72+
73+
if (element.isSealed) {
74+
if (sealedDiscriminators.contains(config.unionDiscriminator)) {
75+
throw InvalidGenerationSource(
76+
'Nested sealed classes cannot have the same discriminator.',
77+
todo: 'Rename one of the discriminators with `unionDiscriminator` '
78+
'field in `@JsonSerializable`.',
79+
);
80+
}
81+
sealedClassImplementations(element).forEach((impl) {
82+
final annotationConfig = jsonSerializableConfig(impl, _generator);
83+
84+
if (annotationConfig == null) {
85+
throw InvalidGenerationSourceError(
86+
'The class `${element.displayName}` is sealed but its '
87+
'implementation `${impl.displayName}` is not annotated with '
88+
'`JsonSerializable`.',
89+
todo: 'Add `@JsonSerializable` annotation to ${impl.displayName}.',
90+
);
91+
}
92+
93+
if (annotationConfig.createToJson != config.createToJson) {
94+
throw InvalidGenerationSourceError(
95+
'The class `${element.displayName}` is sealed but its '
96+
'implementation `${impl.displayName}` has a different '
97+
'`createToJson` option than the base class.',
98+
);
99+
}
100+
});
101+
}
102+
54103
final sortedFields = createSortedFieldSet(element);
55104

56105
// Used to keep track of why a field is ignored. Useful for providing
@@ -118,6 +167,16 @@ class GeneratorHelper extends HelperCore with EncodeHelper, DecodeHelper {
118167
<String>{},
119168
(Set<String> set, fe) {
120169
final jsonKey = nameAccess(fe);
170+
171+
if (sealedDiscriminators.contains(jsonKey)) {
172+
throw InvalidGenerationSourceError(
173+
'The JSON key "$jsonKey" is conflicting with the discriminator '
174+
'of sealed superclass ',
175+
todo: 'Rename the field or the discriminator.',
176+
element: fe,
177+
);
178+
}
179+
121180
if (!set.add(jsonKey)) {
122181
throw InvalidGenerationSourceError(
123182
'More than one field has the JSON key for name "$jsonKey".',

json_serializable/lib/src/utils.dart

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ import 'package:json_annotation/json_annotation.dart';
99
import 'package:source_gen/source_gen.dart';
1010
import 'package:source_helper/source_helper.dart';
1111

12+
import 'settings.dart';
1213
import 'shared_checkers.dart';
1314
import 'type_helpers/config_types.dart';
1415

1516
const _jsonKeyChecker = TypeChecker.fromRuntime(JsonKey);
17+
const _jsonSerializableChecker = TypeChecker.fromRuntime(JsonSerializable);
1618

1719
DartObject? _jsonKeyAnnotation(FieldElement element) =>
1820
_jsonKeyChecker.firstAnnotationOf(element) ??
@@ -168,24 +170,70 @@ ConstructorElement constructorByName(ClassElement classElement, String name) {
168170
return ctor;
169171
}
170172

173+
/// Given a [ClassElement] that is a sealed class, returns all the
174+
/// direct subclasses of the given sealed class, excluding any
175+
/// indirect subclasses (ie. subclasses of subclasses).
176+
///
177+
/// Otherwise, returns an empty iterable.
171178
Iterable<ClassElement> sealedClassImplementations(
172-
ClassElement maybeSealedClass,
179+
ClassElement maybeSealedSuperClass,
173180
) {
174-
if (maybeSealedClass case final sc when sc.isSealed) {
181+
if (maybeSealedSuperClass case final sc when sc.isSealed) {
175182
return LibraryReader(sc.library)
176-
.annotatedWith(
177-
const TypeChecker.fromRuntime(JsonSerializable),
178-
)
179-
.map((e) => e.element)
183+
.allElements
180184
.whereType<ClassElement>()
181185
.where(
182-
(e) => e.allSupertypes.contains(sc.thisType),
186+
(e) =>
187+
e.interfaces.contains(sc.thisType) || e.supertype?.element == sc,
183188
);
184189
}
185190

186191
return const Iterable<ClassElement>.empty();
187192
}
188193

194+
/// Given a [ClassElement] that is a subclass of sealed classes, returns
195+
/// all of the sealed superclasses, including all indirect superclasses
196+
/// (ie. superclasses of superclasses)
197+
///
198+
/// Otherwise, returns an empty iterable.
199+
Iterable<ClassElement> sealedSuperClasses(
200+
ClassElement maybeSealedImplementation,
201+
) =>
202+
maybeSealedImplementation.allSupertypes
203+
.map((type) => type.element)
204+
.whereType<ClassElement>()
205+
.where(
206+
(element) => element.isSealed,
207+
);
208+
209+
/// Given a [ClassElement] that is annotated with `@JsonSerializable`, returns
210+
/// the annotation config merged with build runner config and defaults.
211+
///
212+
/// Otherwise, returns `null`.
213+
ClassConfig? jsonSerializableConfig(
214+
ClassElement maybeAnnotatedElement,
215+
Settings generator,
216+
) {
217+
final maybeSuperAnnotation = _jsonSerializableChecker.firstAnnotationOfExact(
218+
maybeAnnotatedElement,
219+
throwOnUnresolved: false,
220+
);
221+
222+
if (maybeSuperAnnotation case final superAnnotation?) {
223+
final annotationReader = ConstantReader(
224+
superAnnotation,
225+
);
226+
227+
return mergeConfig(
228+
generator.config,
229+
annotationReader,
230+
classElement: maybeAnnotatedElement,
231+
);
232+
}
233+
234+
return null;
235+
}
236+
189237
/// If [targetType] is an enum, returns the [FieldElement] instances associated
190238
/// with its values.
191239
///

0 commit comments

Comments
 (0)