diff --git a/json_annotation/lib/src/json_key.dart b/json_annotation/lib/src/json_key.dart index 1dc7653b..b4da6a07 100644 --- a/json_annotation/lib/src/json_key.dart +++ b/json_annotation/lib/src/json_key.dart @@ -8,7 +8,7 @@ import 'allowed_keys_helpers.dart'; import 'json_serializable.dart'; /// An annotation used to specify how a field is serialized. -@Target({TargetKind.field, TargetKind.getter}) +@Target({TargetKind.field, TargetKind.getter, TargetKind.parameter}) class JsonKey { /// The value to use if the source JSON does not contain this key or if the /// value is `null`. diff --git a/json_serializable/lib/src/json_key_utils.dart b/json_serializable/lib/src/json_key_utils.dart index 4d4f0a4c..ac7fdc7f 100644 --- a/json_serializable/lib/src/json_key_utils.dart +++ b/json_serializable/lib/src/json_key_utils.dart @@ -24,14 +24,28 @@ KeyConfig jsonKeyForField(FieldElement2 field, ClassConfig classAnnotation) => ); KeyConfig _from(FieldElement2 element, ClassConfig classAnnotation) { - // If an annotation exists on `element` the source is a 'real' field. - // If the result is `null`, check the getter – it is a property. - // TODO: setters: github.com/google/json_serializable.dart/issues/24 final obj = jsonKeyAnnotation(element); + final ctorParam = classAnnotation.ctorParams + .where((e) => e.name3 == element.name3) + .singleOrNull; + final ctorObj = ctorParam == null + ? null + : jsonKeyAnnotationForCtorParam(ctorParam); + + ConstantReader fallbackObjRead(String field) { + if (ctorObj != null && !ctorObj.isNull) { + final ctorReadResult = ctorObj.read(field); + if (!ctorReadResult.isNull) return ctorReadResult; + } + if (obj.isNull) { + return ConstantReader(null); + } + return obj.read(field); + } - final ctorParamDefault = classAnnotation.ctorParamDefaults[element.name3]; + final ctorParamDefault = ctorParam?.defaultValueCode; - if (obj.isNull) { + if (obj.isNull && (ctorObj == null || ctorObj.isNull)) { return _populateJsonKey( classAnnotation, element, @@ -121,7 +135,7 @@ KeyConfig _from(FieldElement2 element, ClassConfig classAnnotation) { /// either the annotated field is not an `enum` or `List` or if the value in /// [fieldName] is not an `enum` value. String? createAnnotationValue(String fieldName, {bool mustBeEnum = false}) { - final annotationValue = obj.read(fieldName); + final annotationValue = fallbackObjRead(fieldName); if (annotationValue.isNull) { return null; @@ -228,16 +242,17 @@ KeyConfig _from(FieldElement2 element, ClassConfig classAnnotation) { } String? readValueFunctionName; - final readValue = obj.read('readValue'); + final readValue = fallbackObjRead('readValue'); if (!readValue.isNull) { readValueFunctionName = readValue.objectValue .toFunctionValue2()! .qualifiedName; } - final ignore = obj.read('ignore').literalValue as bool?; - var includeFromJson = obj.read('includeFromJson').literalValue as bool?; - var includeToJson = obj.read('includeToJson').literalValue as bool?; + final ignore = fallbackObjRead('ignore').literalValue as bool?; + var includeFromJson = + fallbackObjRead('includeFromJson').literalValue as bool?; + var includeToJson = fallbackObjRead('includeToJson').literalValue as bool?; if (ignore != null) { if (includeFromJson != null) { @@ -262,11 +277,12 @@ KeyConfig _from(FieldElement2 element, ClassConfig classAnnotation) { classAnnotation, element, defaultValue: defaultValue ?? ctorParamDefault, - disallowNullValue: obj.read('disallowNullValue').literalValue as bool?, - includeIfNull: obj.read('includeIfNull').literalValue as bool?, - name: obj.read('name').literalValue as String?, + disallowNullValue: + fallbackObjRead('disallowNullValue').literalValue as bool?, + includeIfNull: fallbackObjRead('includeIfNull').literalValue as bool?, + name: fallbackObjRead('name').literalValue as String?, readValueFunctionName: readValueFunctionName, - required: obj.read('required').literalValue as bool?, + required: fallbackObjRead('required').literalValue as bool?, unknownEnumValue: createAnnotationValue( 'unknownEnumValue', mustBeEnum: true, diff --git a/json_serializable/lib/src/type_helpers/config_types.dart b/json_serializable/lib/src/type_helpers/config_types.dart index 80d25858..bce6e0ec 100644 --- a/json_serializable/lib/src/type_helpers/config_types.dart +++ b/json_serializable/lib/src/type_helpers/config_types.dart @@ -3,6 +3,7 @@ // BSD-style license that can be found in the LICENSE file. import 'package:analyzer/dart/constant/value.dart'; +import 'package:analyzer/dart/element/element2.dart'; import 'package:json_annotation/json_annotation.dart'; /// Represents values from [JsonKey] when merged with local configuration. @@ -57,7 +58,7 @@ class ClassConfig { final bool genericArgumentFactories; final bool ignoreUnannotated; final bool includeIfNull; - final Map ctorParamDefaults; + final List ctorParams; final List converters; const ClassConfig({ @@ -76,7 +77,7 @@ class ClassConfig { required this.ignoreUnannotated, required this.includeIfNull, this.converters = const [], - this.ctorParamDefaults = const {}, + this.ctorParams = const [], }); factory ClassConfig.fromJsonSerializable(JsonSerializable config) => diff --git a/json_serializable/lib/src/utils.dart b/json_serializable/lib/src/utils.dart index f7a014ab..47f4b1c9 100644 --- a/json_serializable/lib/src/utils.dart +++ b/json_serializable/lib/src/utils.dart @@ -14,6 +14,9 @@ import 'type_helpers/config_types.dart'; const _jsonKeyChecker = TypeChecker.fromRuntime(JsonKey); +// If an annotation exists on `element` the source is a 'real' field. +// If the result is `null`, check the getter – it is a property. +// TODO: setters: github.com/google/json_serializable.dart/issues/24 DartObject? _jsonKeyAnnotation(FieldElement2 element) => _jsonKeyChecker.firstAnnotationOf(element) ?? (element.getter2 == null @@ -27,6 +30,9 @@ ConstantReader jsonKeyAnnotation(FieldElement2 element) => bool hasJsonKeyAnnotation(FieldElement2 element) => _jsonKeyAnnotation(element) != null; +ConstantReader jsonKeyAnnotationForCtorParam(FormalParameterElement element) => + ConstantReader(_jsonKeyChecker.firstAnnotationOf(element)); + Never throwUnsupported(FieldElement2 element, String message) => throw InvalidGenerationSourceError( 'Error with `@JsonKey` on the `${element.name3}` field. $message', @@ -82,7 +88,7 @@ ClassConfig mergeConfig( required ClassElement2 classElement, }) { final annotation = _valueForAnnotation(reader); - assert(config.ctorParamDefaults.isEmpty); + assert(config.ctorParams.isEmpty); final constructor = annotation.constructor ?? config.constructor; final constructorInstance = _constructorByNameOrNull( @@ -90,13 +96,9 @@ ClassConfig mergeConfig( constructor, ); - final paramDefaultValueMap = constructorInstance == null - ? {} - : Map.fromEntries( - constructorInstance.formalParameters - .where((element) => element.hasDefaultValue) - .map((e) => MapEntry(e.name3!, e.defaultValueCode!)), - ); + final ctorParams = [ + ...?constructorInstance?.formalParameters, + ]; final converters = reader.read('converters'); @@ -120,7 +122,7 @@ ClassConfig mergeConfig( config.genericArgumentFactories), ignoreUnannotated: annotation.ignoreUnannotated ?? config.ignoreUnannotated, includeIfNull: annotation.includeIfNull ?? config.includeIfNull, - ctorParamDefaults: paramDefaultValueMap, + ctorParams: ctorParams, converters: converters.isNull ? const [] : converters.listValue, ); } diff --git a/json_serializable/test/json_serializable_test.dart b/json_serializable/test/json_serializable_test.dart index b6ff3678..0f58f71b 100644 --- a/json_serializable/test/json_serializable_test.dart +++ b/json_serializable/test/json_serializable_test.dart @@ -49,6 +49,8 @@ const _expectedAnnotatedTests = { 'BadToFuncReturnType', 'BadTwoRequiredPositional', 'CtorDefaultValueAndJsonKeyDefaultValue', + 'CtorParamJsonKey', + 'CtorParamJsonKeyWithExtends', 'DefaultDoubleConstants', 'DefaultWithConstObject', 'DefaultWithDisallowNullRequiredClass', diff --git a/json_serializable/test/src/_json_serializable_test_input.dart b/json_serializable/test/src/_json_serializable_test_input.dart index 09deed52..20cd8739 100644 --- a/json_serializable/test/src/_json_serializable_test_input.dart +++ b/json_serializable/test/src/_json_serializable_test_input.dart @@ -14,6 +14,7 @@ part 'constants_copy.dart'; part 'core_subclass_type_input.dart'; part 'default_value_input.dart'; part 'field_namer_input.dart'; +part 'extends_jsonkey_override.dart'; part 'generic_test_input.dart'; part 'inheritance_test_input.dart'; part 'json_converter_test_input.dart'; diff --git a/json_serializable/test/src/extends_jsonkey_override.dart b/json_serializable/test/src/extends_jsonkey_override.dart new file mode 100644 index 00000000..989f0e24 --- /dev/null +++ b/json_serializable/test/src/extends_jsonkey_override.dart @@ -0,0 +1,52 @@ +// @dart=3.8 + +part of '_json_serializable_test_input.dart'; + +// https://github.com/google/json_serializable.dart/issues/1437 +@ShouldGenerate(r''' +CtorParamJsonKey _$CtorParamJsonKeyFromJson(Map json) => + CtorParamJsonKey( + attributeOne: json['first'] as String, + attributeTwo: json['second'] as String, + ); + +Map _$CtorParamJsonKeyToJson(CtorParamJsonKey instance) => + { + 'first': instance.attributeOne, + 'second': instance.attributeTwo, + }; +''') +@JsonSerializable() +class CtorParamJsonKey { + CtorParamJsonKey({ + @JsonKey(name: 'first') required this.attributeOne, + @JsonKey(name: 'second') required this.attributeTwo, + }); + + @JsonKey(name: 'fake_first') + final String attributeOne; + final String attributeTwo; +} + +@ShouldGenerate(r''' +CtorParamJsonKeyWithExtends _$CtorParamJsonKeyWithExtendsFromJson( + Map json, +) => CtorParamJsonKeyWithExtends( + attributeOne: json['fake_first'] as String, + attributeTwo: json['two'] as String, +); + +Map _$CtorParamJsonKeyWithExtendsToJson( + CtorParamJsonKeyWithExtends instance, +) => { + 'fake_first': instance.attributeOne, + 'two': instance.attributeTwo, +}; +''') +@JsonSerializable() +class CtorParamJsonKeyWithExtends extends CtorParamJsonKey { + CtorParamJsonKeyWithExtends({ + required super.attributeOne, + @JsonKey(name: 'two') required super.attributeTwo, + }); +}