Skip to content

Commit 4298f97

Browse files
Allow @JsonKey to be used on constructor parameters (#1505)
1 parent 7d8d510 commit 4298f97

File tree

10 files changed

+126
-27
lines changed

10 files changed

+126
-27
lines changed

json_annotation/CHANGELOG.md

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

33
- Require Dart 3.8
4+
- Support `JsonKey` annotation on constructor parameters.
45

56
## 4.9.0
67

json_annotation/lib/src/json_key.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import 'allowed_keys_helpers.dart';
88
import 'json_serializable.dart';
99

1010
/// An annotation used to specify how a field is serialized.
11-
@Target({TargetKind.field, TargetKind.getter})
11+
///
12+
/// This annotation can be used on both class properties and constructor
13+
/// parameters.
14+
@Target({TargetKind.field, TargetKind.getter, TargetKind.parameter})
1215
class JsonKey {
1316
/// The value to use if the source JSON does not contain this key or if the
1417
/// value is `null`.

json_serializable/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 6.11.0
2+
3+
- Support `JsonKey` annotation on constructor parameters.
4+
15
## 6.10.0
26

37
- Required `analyzer: ^7.4.0`.

json_serializable/lib/src/json_key_utils.dart

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,38 @@ KeyConfig jsonKeyForField(FieldElement2 field, ClassConfig classAnnotation) =>
2424
);
2525

2626
KeyConfig _from(FieldElement2 element, ClassConfig classAnnotation) {
27-
// If an annotation exists on `element` the source is a 'real' field.
28-
// If the result is `null`, check the getter – it is a property.
29-
// TODO: setters: github.com/google/json_serializable.dart/issues/24
3027
final obj = jsonKeyAnnotation(element);
28+
final ctorParam = classAnnotation.ctorParams
29+
.where((e) => e.name3 == element.name3)
30+
.singleOrNull;
31+
final ctorObj = ctorParam == null
32+
? null
33+
: jsonKeyAnnotationForCtorParam(ctorParam);
34+
35+
ConstantReader fallbackObjRead(String field) {
36+
if (ctorObj != null && !ctorObj.isNull) {
37+
final ctorReadResult = ctorObj.read(field);
38+
if (!ctorReadResult.isNull) {
39+
if (!obj.isNull && !obj.read(field).isNull) {
40+
log.warning(
41+
'Field `${element.name3}` has conflicting `JsonKey.$field` '
42+
'annotations: both constructor parameter and class field have '
43+
'this annotation. Using constructor parameter value.',
44+
);
45+
}
46+
47+
return ctorReadResult;
48+
}
49+
}
50+
if (obj.isNull) {
51+
return ConstantReader(null);
52+
}
53+
return obj.read(field);
54+
}
3155

32-
final ctorParamDefault = classAnnotation.ctorParamDefaults[element.name3];
56+
final ctorParamDefault = ctorParam?.defaultValueCode;
3357

34-
if (obj.isNull) {
58+
if (obj.isNull && (ctorObj == null || ctorObj.isNull)) {
3559
return _populateJsonKey(
3660
classAnnotation,
3761
element,
@@ -121,7 +145,7 @@ KeyConfig _from(FieldElement2 element, ClassConfig classAnnotation) {
121145
/// either the annotated field is not an `enum` or `List` or if the value in
122146
/// [fieldName] is not an `enum` value.
123147
String? createAnnotationValue(String fieldName, {bool mustBeEnum = false}) {
124-
final annotationValue = obj.read(fieldName);
148+
final annotationValue = fallbackObjRead(fieldName);
125149

126150
if (annotationValue.isNull) {
127151
return null;
@@ -228,16 +252,17 @@ KeyConfig _from(FieldElement2 element, ClassConfig classAnnotation) {
228252
}
229253

230254
String? readValueFunctionName;
231-
final readValue = obj.read('readValue');
255+
final readValue = fallbackObjRead('readValue');
232256
if (!readValue.isNull) {
233257
readValueFunctionName = readValue.objectValue
234258
.toFunctionValue2()!
235259
.qualifiedName;
236260
}
237261

238-
final ignore = obj.read('ignore').literalValue as bool?;
239-
var includeFromJson = obj.read('includeFromJson').literalValue as bool?;
240-
var includeToJson = obj.read('includeToJson').literalValue as bool?;
262+
final ignore = fallbackObjRead('ignore').literalValue as bool?;
263+
var includeFromJson =
264+
fallbackObjRead('includeFromJson').literalValue as bool?;
265+
var includeToJson = fallbackObjRead('includeToJson').literalValue as bool?;
241266

242267
if (ignore != null) {
243268
if (includeFromJson != null) {
@@ -262,11 +287,12 @@ KeyConfig _from(FieldElement2 element, ClassConfig classAnnotation) {
262287
classAnnotation,
263288
element,
264289
defaultValue: defaultValue ?? ctorParamDefault,
265-
disallowNullValue: obj.read('disallowNullValue').literalValue as bool?,
266-
includeIfNull: obj.read('includeIfNull').literalValue as bool?,
267-
name: obj.read('name').literalValue as String?,
290+
disallowNullValue:
291+
fallbackObjRead('disallowNullValue').literalValue as bool?,
292+
includeIfNull: fallbackObjRead('includeIfNull').literalValue as bool?,
293+
name: fallbackObjRead('name').literalValue as String?,
268294
readValueFunctionName: readValueFunctionName,
269-
required: obj.read('required').literalValue as bool?,
295+
required: fallbackObjRead('required').literalValue as bool?,
270296
unknownEnumValue: createAnnotationValue(
271297
'unknownEnumValue',
272298
mustBeEnum: true,

json_serializable/lib/src/type_helpers/config_types.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// BSD-style license that can be found in the LICENSE file.
44

55
import 'package:analyzer/dart/constant/value.dart';
6+
import 'package:analyzer/dart/element/element2.dart';
67
import 'package:json_annotation/json_annotation.dart';
78

89
/// Represents values from [JsonKey] when merged with local configuration.
@@ -57,7 +58,7 @@ class ClassConfig {
5758
final bool genericArgumentFactories;
5859
final bool ignoreUnannotated;
5960
final bool includeIfNull;
60-
final Map<String, String> ctorParamDefaults;
61+
final List<FormalParameterElement> ctorParams;
6162
final List<DartObject> converters;
6263

6364
const ClassConfig({
@@ -76,7 +77,7 @@ class ClassConfig {
7677
required this.ignoreUnannotated,
7778
required this.includeIfNull,
7879
this.converters = const [],
79-
this.ctorParamDefaults = const {},
80+
this.ctorParams = const [],
8081
});
8182

8283
factory ClassConfig.fromJsonSerializable(JsonSerializable config) =>

json_serializable/lib/src/utils.dart

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import 'type_helpers/config_types.dart';
1414

1515
const _jsonKeyChecker = TypeChecker.fromRuntime(JsonKey);
1616

17+
/// If an annotation exists on `element` the source is a 'real' field.
18+
/// If the result is `null`, check the getter – it is a property.
19+
// TODO: setters: github.com/google/json_serializable.dart/issues/24
1720
DartObject? _jsonKeyAnnotation(FieldElement2 element) =>
1821
_jsonKeyChecker.firstAnnotationOf(element) ??
1922
(element.getter2 == null
@@ -27,6 +30,9 @@ ConstantReader jsonKeyAnnotation(FieldElement2 element) =>
2730
bool hasJsonKeyAnnotation(FieldElement2 element) =>
2831
_jsonKeyAnnotation(element) != null;
2932

33+
ConstantReader jsonKeyAnnotationForCtorParam(FormalParameterElement element) =>
34+
ConstantReader(_jsonKeyChecker.firstAnnotationOf(element));
35+
3036
Never throwUnsupported(FieldElement2 element, String message) =>
3137
throw InvalidGenerationSourceError(
3238
'Error with `@JsonKey` on the `${element.name3}` field. $message',
@@ -82,21 +88,17 @@ ClassConfig mergeConfig(
8288
required ClassElement2 classElement,
8389
}) {
8490
final annotation = _valueForAnnotation(reader);
85-
assert(config.ctorParamDefaults.isEmpty);
91+
assert(config.ctorParams.isEmpty);
8692

8793
final constructor = annotation.constructor ?? config.constructor;
8894
final constructorInstance = _constructorByNameOrNull(
8995
classElement,
9096
constructor,
9197
);
9298

93-
final paramDefaultValueMap = constructorInstance == null
94-
? <String, String>{}
95-
: Map<String, String>.fromEntries(
96-
constructorInstance.formalParameters
97-
.where((element) => element.hasDefaultValue)
98-
.map((e) => MapEntry(e.name3!, e.defaultValueCode!)),
99-
);
99+
final ctorParams = <FormalParameterElement>[
100+
...?constructorInstance?.formalParameters,
101+
];
100102

101103
final converters = reader.read('converters');
102104

@@ -120,7 +122,7 @@ ClassConfig mergeConfig(
120122
config.genericArgumentFactories),
121123
ignoreUnannotated: annotation.ignoreUnannotated ?? config.ignoreUnannotated,
122124
includeIfNull: annotation.includeIfNull ?? config.includeIfNull,
123-
ctorParamDefaults: paramDefaultValueMap,
125+
ctorParams: ctorParams,
124126
converters: converters.isNull ? const [] : converters.listValue,
125127
);
126128
}

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.10.0
2+
version: 6.11.0
33
description: >-
44
Automatically generate code for converting to and from JSON by annotating
55
Dart classes.

json_serializable/test/json_serializable_test.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ const _expectedAnnotatedTests = {
4949
'BadToFuncReturnType',
5050
'BadTwoRequiredPositional',
5151
'CtorDefaultValueAndJsonKeyDefaultValue',
52+
'CtorParamJsonKey',
53+
'CtorParamJsonKeyWithExtends',
5254
'DefaultDoubleConstants',
5355
'DefaultWithConstObject',
5456
'DefaultWithDisallowNullRequiredClass',

json_serializable/test/src/_json_serializable_test_input.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ part 'constants_copy.dart';
1414
part 'core_subclass_type_input.dart';
1515
part 'default_value_input.dart';
1616
part 'field_namer_input.dart';
17+
part 'extends_jsonkey_override.dart';
1718
part 'generic_test_input.dart';
1819
part 'inheritance_test_input.dart';
1920
part 'json_converter_test_input.dart';
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// @dart=3.8
2+
3+
part of '_json_serializable_test_input.dart';
4+
5+
// https://github.com/google/json_serializable.dart/issues/1437
6+
@ShouldGenerate(
7+
r'''
8+
CtorParamJsonKey _$CtorParamJsonKeyFromJson(Map<String, dynamic> json) =>
9+
CtorParamJsonKey(
10+
attributeOne: json['first'] as String,
11+
attributeTwo: json['second'] as String,
12+
);
13+
14+
Map<String, dynamic> _$CtorParamJsonKeyToJson(CtorParamJsonKey instance) =>
15+
<String, dynamic>{
16+
'first': instance.attributeOne,
17+
'second': instance.attributeTwo,
18+
};
19+
''',
20+
expectedLogItems: [
21+
'Field `attributeOne` has conflicting `JsonKey.name` annotations: both '
22+
'constructor parameter and class field have this annotation. Using '
23+
'constructor parameter value.',
24+
],
25+
)
26+
@JsonSerializable()
27+
class CtorParamJsonKey {
28+
CtorParamJsonKey({
29+
@JsonKey(name: 'first') required this.attributeOne,
30+
@JsonKey(name: 'second') required this.attributeTwo,
31+
});
32+
33+
@JsonKey(name: 'fake_first')
34+
final String attributeOne;
35+
final String attributeTwo;
36+
}
37+
38+
@ShouldGenerate(r'''
39+
CtorParamJsonKeyWithExtends _$CtorParamJsonKeyWithExtendsFromJson(
40+
Map<String, dynamic> json,
41+
) => CtorParamJsonKeyWithExtends(
42+
attributeOne: json['fake_first'] as String,
43+
attributeTwo: json['two'] as String,
44+
);
45+
46+
Map<String, dynamic> _$CtorParamJsonKeyWithExtendsToJson(
47+
CtorParamJsonKeyWithExtends instance,
48+
) => <String, dynamic>{
49+
'fake_first': instance.attributeOne,
50+
'two': instance.attributeTwo,
51+
};
52+
''')
53+
@JsonSerializable()
54+
class CtorParamJsonKeyWithExtends extends CtorParamJsonKey {
55+
CtorParamJsonKeyWithExtends({
56+
required super.attributeOne,
57+
@JsonKey(name: 'two') required super.attributeTwo,
58+
});
59+
}

0 commit comments

Comments
 (0)