Skip to content

Commit 8725b6d

Browse files
committed
Add @JsonEnum annotation
Allows generating the associated helpers without requiring usage as a field in a @JsonSerializable class Fixes #778
1 parent 83b71d2 commit 8725b6d

15 files changed

+169
-7
lines changed

json_annotation/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
- Added `JsonSerializabel.constructor` field to allow specifying an alternative
44
constructor to invoke when creating a `fromJson` helper.
5+
- Added `JsonEnum` for annotating `enum` types.
56

67
## 4.1.0
78

json_annotation/lib/json_annotation.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ library json_annotation;
1313
export 'src/allowed_keys_helpers.dart';
1414
export 'src/checked_helpers.dart';
1515
export 'src/json_converter.dart';
16+
export 'src/json_enum.dart';
1617
export 'src/json_key.dart';
1718
export 'src/json_literal.dart';
1819
export 'src/json_serializable.dart';
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:meta/meta_meta.dart';
6+
7+
/// When applied to `enum` definitions, causes the corresponding private
8+
// `_$EnumNameEnumMap` and `_$enumDecode` helpers to be generated, even if the
9+
// `enum` is not referenced elsewhere in generated code.
10+
@Target({TargetKind.enumType})
11+
class JsonEnum {
12+
const JsonEnum();
13+
}

json_serializable/CHANGELOG.md

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

33
- Added support for `JsonSerializabel.constructor` to allow specifying an
44
alternative constructor to invoke when creating a `fromJson` helper.
5+
- Support the new `@JsonEnum` annotation by generating the corresponding private
6+
`_$EnumNameEnumMap` and `_$enumDecode` helpers, even if the `enum` is not
7+
referenced elsewhere in generated code.
58
- Require `json_annotation` `'>=4.2.0 <4.3.0'`.
69

710
## 5.0.2

json_serializable/lib/json_serializable.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
export 'src/json_enum_generator.dart' show JsonEnumGenerator;
56
export 'src/json_literal_generator.dart' show JsonLiteralGenerator;
67
export 'src/json_serializable_generator.dart' show JsonSerializableGenerator;
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:analyzer/dart/element/element.dart';
6+
import 'package:build/build.dart';
7+
import 'package:json_annotation/json_annotation.dart';
8+
import 'package:source_gen/source_gen.dart';
9+
10+
import 'type_helpers/enum_helper.dart';
11+
12+
class JsonEnumGenerator extends GeneratorForAnnotation<JsonEnum> {
13+
const JsonEnumGenerator();
14+
15+
@override
16+
List<String> generateForAnnotatedElement(
17+
Element element,
18+
ConstantReader annotation,
19+
BuildStep buildStep,
20+
) {
21+
if (element is! ClassElement || !element.isEnum) {
22+
throw InvalidGenerationSourceError(
23+
'`@JsonEnum` can only be used on enum elements.',
24+
element: element,
25+
);
26+
}
27+
28+
return [
29+
enumDecodeHelper,
30+
enumValueMapFromType(element.thisType)!,
31+
];
32+
}
33+
}

json_serializable/lib/src/json_part_builder.dart

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:build/build.dart';
66
import 'package:json_annotation/json_annotation.dart';
77
import 'package:source_gen/source_gen.dart';
88

9+
import 'json_enum_generator.dart';
910
import 'json_literal_generator.dart';
1011
import 'json_serializable_generator.dart';
1112
import 'settings.dart';
@@ -23,10 +24,70 @@ Builder jsonPartBuilder({
2324

2425
return SharedPartBuilder(
2526
[
26-
JsonSerializableGenerator.fromSettings(settings),
27+
_UnifiedGenerator([
28+
JsonSerializableGenerator.fromSettings(settings),
29+
const JsonEnumGenerator(),
30+
]),
2731
const JsonLiteralGenerator(),
2832
],
2933
'json_serializable',
3034
formatOutput: formatOutput,
3135
);
3236
}
37+
38+
/// Allows exposing separate [GeneratorForAnnotation] instances as one
39+
/// generator.
40+
///
41+
/// Output can be merged while keeping implementations separate.
42+
class _UnifiedGenerator extends Generator {
43+
final List<GeneratorForAnnotation> _generators;
44+
45+
_UnifiedGenerator(this._generators);
46+
47+
@override
48+
Future<String?> generate(LibraryReader library, BuildStep buildStep) async {
49+
final values = <String>{};
50+
51+
for (var generator in _generators) {
52+
for (var annotatedElement
53+
in library.annotatedWith(generator.typeChecker)) {
54+
final generatedValue = generator.generateForAnnotatedElement(
55+
annotatedElement.element, annotatedElement.annotation, buildStep);
56+
for (var value in _normalizeGeneratorOutput(generatedValue)) {
57+
assert(value.length == value.trim().length);
58+
values.add(value);
59+
}
60+
}
61+
}
62+
63+
return values.join('\n\n');
64+
}
65+
66+
@override
67+
String toString() => 'JsonSerializableGenerator';
68+
}
69+
70+
// Borrowed from `package:source_gen`
71+
Iterable<String> _normalizeGeneratorOutput(Object? value) {
72+
if (value == null) {
73+
return const [];
74+
} else if (value is String) {
75+
value = [value];
76+
}
77+
78+
if (value is Iterable) {
79+
return value.where((e) => e != null).map((e) {
80+
if (e is String) {
81+
return e.trim();
82+
}
83+
84+
throw _argError(e as Object);
85+
}).where((e) => e.isNotEmpty);
86+
}
87+
throw _argError(value);
88+
}
89+
90+
// Borrowed from `package:source_gen`
91+
ArgumentError _argError(Object value) => ArgumentError(
92+
'Must be a String or be an Iterable containing String values. '
93+
'Found `${Error.safeToString(value)}` (${value.runtimeType}).');

json_serializable/lib/src/json_serializable_generator.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ class JsonSerializableGenerator
6969
);
7070
}
7171

72-
if (element is! ClassElement) {
72+
if (element is! ClassElement || element.isEnum) {
7373
throw InvalidGenerationSourceError(
7474
'`@JsonSerializable` can only be used on classes.',
7575
element: element,

json_serializable/lib/src/type_helpers/enum_helper.dart

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class EnumHelper extends TypeHelper<TypeHelperContextWithConfig> {
2121
String expression,
2222
TypeHelperContextWithConfig context,
2323
) {
24-
final memberContent = _enumValueMapFromType(targetType);
24+
final memberContent = enumValueMapFromType(targetType);
2525

2626
if (memberContent == null) {
2727
return null;
@@ -39,13 +39,13 @@ class EnumHelper extends TypeHelper<TypeHelperContextWithConfig> {
3939
TypeHelperContextWithConfig context,
4040
bool defaultProvided,
4141
) {
42-
final memberContent = _enumValueMapFromType(targetType);
42+
final memberContent = enumValueMapFromType(targetType);
4343

4444
if (memberContent == null) {
4545
return null;
4646
}
4747

48-
context.addMember(_enumDecodeHelper);
48+
context.addMember(enumDecodeHelper);
4949

5050
String functionName;
5151
if (targetType.isNullableType || defaultProvided) {
@@ -72,7 +72,7 @@ class EnumHelper extends TypeHelper<TypeHelperContextWithConfig> {
7272
String _constMapName(DartType targetType) =>
7373
'_\$${targetType.element!.name}EnumMap';
7474

75-
String? _enumValueMapFromType(DartType targetType) {
75+
String? enumValueMapFromType(DartType targetType) {
7676
final enumMap = enumFieldsMap(targetType);
7777

7878
if (enumMap == null) {
@@ -87,7 +87,7 @@ String? _enumValueMapFromType(DartType targetType) {
8787
return 'const ${_constMapName(targetType)} = {\n$items\n};';
8888
}
8989

90-
const _enumDecodeHelper = r'''
90+
const enumDecodeHelper = r'''
9191
K _$enumDecode<K, V>(
9292
Map<K, V> enumValues,
9393
Object? source, {

json_serializable/lib/src/type_helpers/json_helper.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,9 @@ InterfaceType? _instantiate(
270270
}
271271

272272
ClassConfig? _annotation(ClassConfig config, InterfaceType source) {
273+
if (source.isEnum) {
274+
return null;
275+
}
273276
final annotations = const TypeChecker.fromRuntime(JsonSerializable)
274277
.annotationsOfExact(source.element, throwOnUnresolved: false)
275278
.toList();

json_serializable/test/integration/integration_test.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,4 +296,8 @@ void main() {
296296

297297
validateRoundTrip(value, (json) => PrivateConstructor.fromJson(json));
298298
});
299+
300+
test('enum helpers', () {
301+
expect(standAloneEnumKeys, ['a', 'b', 'g', 'd']);
302+
});
299303
}

json_serializable/test/integration/json_test_example.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,3 +239,17 @@ class PrivateConstructor {
239239
bool operator ==(Object other) =>
240240
other is PrivateConstructor && id == other.id && value == other.value;
241241
}
242+
243+
@JsonEnum()
244+
enum StandAloneEnum {
245+
@JsonValue('a')
246+
alpha,
247+
@JsonValue('b')
248+
beta,
249+
@JsonValue('g')
250+
gamma,
251+
@JsonValue('d')
252+
delta,
253+
}
254+
255+
Iterable<String> get standAloneEnumKeys => _$StandAloneEnumEnumMap.values;

json_serializable/test/integration/json_test_example.g.dart

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

json_serializable/test/integration/json_test_example.g_any_map.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,3 +247,17 @@ class PrivateConstructor {
247247
bool operator ==(Object other) =>
248248
other is PrivateConstructor && id == other.id && value == other.value;
249249
}
250+
251+
@JsonEnum()
252+
enum StandAloneEnum {
253+
@JsonValue('a')
254+
alpha,
255+
@JsonValue('b')
256+
beta,
257+
@JsonValue('g')
258+
gamma,
259+
@JsonValue('d')
260+
delta,
261+
}
262+
263+
Iterable<String> get standAloneEnumKeys => _$StandAloneEnumEnumMap.values;

json_serializable/test/integration/json_test_example.g_any_map.g.dart

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

0 commit comments

Comments
 (0)