Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion json_annotation/lib/src/json_key.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
44 changes: 30 additions & 14 deletions json_serializable/lib/src/json_key_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <FormalParameterElement?>[
...classAnnotation.ctorParams,
].singleWhere((e) => e!.name3 == element.name3, orElse: () => null);
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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand All @@ -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,
Expand Down
5 changes: 3 additions & 2 deletions json_serializable/lib/src/type_helpers/config_types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -57,7 +58,7 @@ class ClassConfig {
final bool genericArgumentFactories;
final bool ignoreUnannotated;
final bool includeIfNull;
final Map<String, String> ctorParamDefaults;
final List<FormalParameterElement> ctorParams;
final List<DartObject> converters;

const ClassConfig({
Expand All @@ -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) =>
Expand Down
20 changes: 11 additions & 9 deletions json_serializable/lib/src/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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',
Expand Down Expand Up @@ -82,21 +88,17 @@ 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(
classElement,
constructor,
);

final paramDefaultValueMap = constructorInstance == null
? <String, String>{}
: Map<String, String>.fromEntries(
constructorInstance.formalParameters
.where((element) => element.hasDefaultValue)
.map((e) => MapEntry(e.name3!, e.defaultValueCode!)),
);
final ctorParams = <FormalParameterElement>[
...?constructorInstance?.formalParameters,
];

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

Expand All @@ -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,
);
}
Expand Down
2 changes: 2 additions & 0 deletions json_serializable/test/json_serializable_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ const _expectedAnnotatedTests = {
'BadToFuncReturnType',
'BadTwoRequiredPositional',
'CtorDefaultValueAndJsonKeyDefaultValue',
'CtorParamJsonKey',
'CtorParamJsonKeyWithExtends',
'DefaultDoubleConstants',
'DefaultWithConstObject',
'DefaultWithDisallowNullRequiredClass',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
52 changes: 52 additions & 0 deletions json_serializable/test/src/extends_jsonkey_override.dart
Original file line number Diff line number Diff line change
@@ -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<String, dynamic> json) =>
CtorParamJsonKey(
attributeOne: json['first'] as String,
attributeTwo: json['second'] as String,
);

Map<String, dynamic> _$CtorParamJsonKeyToJson(CtorParamJsonKey instance) =>
<String, dynamic>{
'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<String, dynamic> json,
) => CtorParamJsonKeyWithExtends(
attributeOne: json['fake_first'] as String,
attributeTwo: json['two'] as String,
);

Map<String, dynamic> _$CtorParamJsonKeyWithExtendsToJson(
CtorParamJsonKeyWithExtends instance,
) => <String, dynamic>{
'fake_first': instance.attributeOne,
'two': instance.attributeTwo,
};
''')
@JsonSerializable()
class CtorParamJsonKeyWithExtends extends CtorParamJsonKey {
CtorParamJsonKeyWithExtends({
required super.attributeOne,
@JsonKey(name: 'two') required super.attributeTwo,
});
}