Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add JsonConverter(converters) option #1135

Merged
merged 13 commits into from May 24, 2022
Merged
36 changes: 36 additions & 0 deletions json_annotation/lib/src/json_serializable.dart
Expand Up @@ -6,6 +6,7 @@ import 'package:meta/meta_meta.dart';

import 'allowed_keys_helpers.dart';
import 'checked_helpers.dart';
import 'json_converter.dart';
kevmoo marked this conversation as resolved.
Show resolved Hide resolved
import 'json_key.dart';

part 'json_serializable.g.dart';
Expand Down Expand Up @@ -190,6 +191,40 @@ class JsonSerializable {
/// `includeIfNull`, that value takes precedent.
final bool? includeIfNull;

/// A list of [JsonConverter] to apply to this class.
///
/// Writing:
///
/// ```dart
/// @JsonSerializable(converters: [MyJsonConverter()])
/// class Example {...}
/// ```
///
/// is equivalent to writing:
///
/// ```dart
/// @JsonSerializable()
/// @MyJsonConverter()
/// class Example {...}
/// ```
///
/// The main difference is that this allows reusing a custom
/// [JsonSerializable] over multiple classes:
///
/// ```dart
/// const myCustomAnnotation = JsonSerializable(
/// converters: [MyJsonConverter()],
/// );
///
/// @myCustomAnnotation
/// class Example {...}
///
/// @myCustomAnnotation
/// class Another {...}
/// ```
@JsonKey(ignore: true)
final List<JsonConverter>? converters;

/// Creates a new [JsonSerializable] instance.
const JsonSerializable({
@Deprecated('Has no effect') bool? nullable,
Expand All @@ -203,6 +238,7 @@ class JsonSerializable {
this.fieldRename,
this.ignoreUnannotated,
this.includeIfNull,
this.converters,
this.genericArgumentFactories,
});

Expand Down
2 changes: 1 addition & 1 deletion json_serializable/lib/src/json_serializable_generator.dart
Expand Up @@ -20,7 +20,7 @@ class JsonSerializableGenerator
extends GeneratorForAnnotation<JsonSerializable> {
final Settings _settings;

JsonSerializable get config => _settings.config;
JsonSerializable get config => _settings.config.toJsonSerializable();

JsonSerializableGenerator.fromSettings(this._settings);

Expand Down
29 changes: 5 additions & 24 deletions json_serializable/lib/src/settings.dart
Expand Up @@ -43,38 +43,19 @@ class Settings {
GenericFactoryHelper(),
].followedBy(_typeHelpers).followedBy(_coreHelpers);

final JsonSerializable _config;

// #CHANGE WHEN UPDATING json_annotation
ClassConfig get config => ClassConfig(
checked: _config.checked ?? ClassConfig.defaults.checked,
anyMap: _config.anyMap ?? ClassConfig.defaults.anyMap,
constructor: _config.constructor ?? ClassConfig.defaults.constructor,
createFactory:
_config.createFactory ?? ClassConfig.defaults.createFactory,
createToJson: _config.createToJson ?? ClassConfig.defaults.createToJson,
ignoreUnannotated:
_config.ignoreUnannotated ?? ClassConfig.defaults.ignoreUnannotated,
explicitToJson:
_config.explicitToJson ?? ClassConfig.defaults.explicitToJson,
includeIfNull:
_config.includeIfNull ?? ClassConfig.defaults.includeIfNull,
genericArgumentFactories: _config.genericArgumentFactories ??
ClassConfig.defaults.genericArgumentFactories,
fieldRename: _config.fieldRename ?? ClassConfig.defaults.fieldRename,
disallowUnrecognizedKeys: _config.disallowUnrecognizedKeys ??
ClassConfig.defaults.disallowUnrecognizedKeys,
);
final ClassConfig config;

/// Creates an instance of [Settings].
///
/// If [typeHelpers] is not provided, the built-in helpers are used:
/// [BigIntHelper], [DateTimeHelper], [DurationHelper], [JsonHelper], and
/// [UriHelper].
const Settings({
Settings({
JsonSerializable? config,
List<TypeHelper>? typeHelpers,
}) : _config = config ?? ClassConfig.defaults,
}) : config = config != null
? ClassConfig.fromJsonSerializable(config)
: ClassConfig.defaults,
_typeHelpers = typeHelpers ?? defaultHelpers;

/// Creates an instance of [Settings].
Expand Down
93 changes: 41 additions & 52 deletions json_serializable/lib/src/type_helpers/config_types.dart
Expand Up @@ -2,6 +2,7 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:analyzer/dart/constant/value.dart';
import 'package:json_annotation/json_annotation.dart';

/// Represents values from [JsonKey] when merged with local configuration.
Expand Down Expand Up @@ -38,41 +39,20 @@ class KeyConfig {
/// configuration.
///
/// Values are all known, so types are non-nullable.
class ClassConfig implements JsonSerializable {
@override
class ClassConfig {
kevmoo marked this conversation as resolved.
Show resolved Hide resolved
final bool anyMap;

@override
final bool checked;

@override
final String constructor;

@override
final bool createFactory;

@override
final bool createToJson;

@override
final bool disallowUnrecognizedKeys;

@override
final bool explicitToJson;

@override
final FieldRename fieldRename;

@override
final bool genericArgumentFactories;

@override
final bool ignoreUnannotated;

@override
final bool includeIfNull;

final Map<String, String> ctorParamDefaults;
final List<DartObject> converters;

const ClassConfig({
required this.anyMap,
Expand All @@ -86,9 +66,33 @@ class ClassConfig implements JsonSerializable {
required this.genericArgumentFactories,
required this.ignoreUnannotated,
required this.includeIfNull,
this.converters = const [],
this.ctorParamDefaults = const {},
});

factory ClassConfig.fromJsonSerializable(JsonSerializable config) =>
// #CHANGE WHEN UPDATING json_annotation
ClassConfig(
checked: config.checked ?? ClassConfig.defaults.checked,
anyMap: config.anyMap ?? ClassConfig.defaults.anyMap,
constructor: config.constructor ?? ClassConfig.defaults.constructor,
createFactory:
config.createFactory ?? ClassConfig.defaults.createFactory,
createToJson: config.createToJson ?? ClassConfig.defaults.createToJson,
ignoreUnannotated:
config.ignoreUnannotated ?? ClassConfig.defaults.ignoreUnannotated,
explicitToJson:
config.explicitToJson ?? ClassConfig.defaults.explicitToJson,
includeIfNull:
config.includeIfNull ?? ClassConfig.defaults.includeIfNull,
genericArgumentFactories: config.genericArgumentFactories ??
ClassConfig.defaults.genericArgumentFactories,
fieldRename: config.fieldRename ?? ClassConfig.defaults.fieldRename,
disallowUnrecognizedKeys: config.disallowUnrecognizedKeys ??
ClassConfig.defaults.disallowUnrecognizedKeys,
// TODO typeConverters = []
);

/// An instance of [JsonSerializable] with all fields set to their default
/// values.
static const defaults = ClassConfig(
Expand All @@ -105,33 +109,18 @@ class ClassConfig implements JsonSerializable {
includeIfNull: true,
);

@override
Map<String, dynamic> toJson() => _$JsonSerializableToJson(this);

@override
JsonSerializable withDefaults() => this;
JsonSerializable toJsonSerializable() => JsonSerializable(
checked: checked,
anyMap: anyMap,
constructor: constructor,
createFactory: createFactory,
createToJson: createToJson,
ignoreUnannotated: ignoreUnannotated,
explicitToJson: explicitToJson,
includeIfNull: includeIfNull,
genericArgumentFactories: genericArgumentFactories,
fieldRename: fieldRename,
disallowUnrecognizedKeys: disallowUnrecognizedKeys,
// TODO typeConverters = []
);
}

const _$FieldRenameEnumMap = {
FieldRename.none: 'none',
FieldRename.kebab: 'kebab',
FieldRename.snake: 'snake',
FieldRename.pascal: 'pascal',
FieldRename.screamingSnake: 'screamingSnake',
};

// #CHANGE WHEN UPDATING json_annotation
Map<String, dynamic> _$JsonSerializableToJson(JsonSerializable instance) =>
<String, dynamic>{
'any_map': instance.anyMap,
'checked': instance.checked,
'constructor': instance.constructor,
'create_factory': instance.createFactory,
'create_to_json': instance.createToJson,
'disallow_unrecognized_keys': instance.disallowUnrecognizedKeys,
'explicit_to_json': instance.explicitToJson,
'field_rename': _$FieldRenameEnumMap[instance.fieldRename],
'generic_argument_factories': instance.genericArgumentFactories,
'ignore_unannotated': instance.ignoreUnannotated,
'include_if_null': instance.includeIfNull,
};
47 changes: 32 additions & 15 deletions json_serializable/lib/src/type_helpers/json_converter_helper.dart
Expand Up @@ -17,14 +17,14 @@ import '../utils.dart';

/// A [TypeHelper] that supports classes annotated with implementations of
/// [JsonConverter].
class JsonConverterHelper extends TypeHelper {
class JsonConverterHelper extends TypeHelper<TypeHelperContextWithConfig> {
const JsonConverterHelper();

@override
Object? serialize(
DartType targetType,
String expression,
TypeHelperContext context,
TypeHelperContextWithConfig context,
) {
final converter = _typeConverter(targetType, context);

Expand Down Expand Up @@ -57,7 +57,7 @@ Json? $converterToJsonName<Json, Value>(
Object? deserialize(
DartType targetType,
String expression,
TypeHelperContext context,
TypeHelperContextWithConfig context,
bool defaultProvided,
) {
final converter = _typeConverter(targetType, context);
Expand Down Expand Up @@ -143,9 +143,18 @@ class _JsonConvertData {
accessor.isEmpty ? '' : '.$accessor';
}

_JsonConvertData? _typeConverter(DartType targetType, TypeHelperContext ctx) {
_JsonConvertData? _typeConverter(
DartType targetType,
TypeHelperContextWithConfig ctx,
) {
List<_ConverterMatch> converterMatches(List<ElementAnnotation> items) => items
.map((annotation) => _compatibleMatch(targetType, annotation))
.map(
(annotation) => _compatibleMatch(
targetType,
annotation,
annotation.computeConstantValue()!,
),
)
.whereType<_ConverterMatch>()
.toList();

Expand All @@ -157,6 +166,13 @@ _JsonConvertData? _typeConverter(DartType targetType, TypeHelperContext ctx) {

if (matchingAnnotations.isEmpty) {
matchingAnnotations = converterMatches(ctx.classElement.metadata);

if (matchingAnnotations.isEmpty) {
matchingAnnotations = ctx.config.converters
.map((e) => _compatibleMatch(targetType, null, e))
.whereType<_ConverterMatch>()
.toList();
}
}
}

Expand All @@ -174,13 +190,14 @@ _JsonConvertData? _typeConverterFrom(
if (matchingAnnotations.length > 1) {
final targetTypeCode = typeToCode(targetType);
throw InvalidGenerationSourceError(
'Found more than one matching converter for `$targetTypeCode`.',
element: matchingAnnotations[1].elementAnnotation.element);
'Found more than one matching converter for `$targetTypeCode`.',
element: matchingAnnotations[1].elementAnnotation?.element,
);
}

final match = matchingAnnotations.single;

final annotationElement = match.elementAnnotation.element;
final annotationElement = match.elementAnnotation?.element;
if (annotationElement is PropertyAccessorElement) {
final enclosing = annotationElement.enclosingElement;

Expand All @@ -202,8 +219,9 @@ _JsonConvertData? _typeConverterFrom(
if (reviver.namedArguments.isNotEmpty ||
reviver.positionalArguments.isNotEmpty) {
throw InvalidGenerationSourceError(
'Generators with constructor arguments are not supported.',
element: match.elementAnnotation.element);
'Generators with constructor arguments are not supported.',
element: match.elementAnnotation?.element,
);
}

if (match.genericTypeArg != null) {
Expand All @@ -228,7 +246,7 @@ class _ConverterMatch {
final DartObject annotation;
final DartType fieldType;
final DartType jsonType;
final ElementAnnotation elementAnnotation;
final ElementAnnotation? elementAnnotation;
final String? genericTypeArg;

_ConverterMatch(
Expand All @@ -242,10 +260,9 @@ class _ConverterMatch {

_ConverterMatch? _compatibleMatch(
DartType targetType,
ElementAnnotation annotation,
ElementAnnotation? annotation,
DartObject constantValue,
) {
final constantValue = annotation.computeConstantValue()!;

final converterClassElement = constantValue.type!.element as ClassElement;

final jsonConverterSuper =
Expand Down Expand Up @@ -274,7 +291,7 @@ _ConverterMatch? _compatibleMatch(
}

if (fieldType is TypeParameterType && targetType is TypeParameterType) {
assert(annotation.element is! PropertyAccessorElement);
assert(annotation?.element is! PropertyAccessorElement);
assert(converterClassElement.typeParameters.isNotEmpty);
if (converterClassElement.typeParameters.length > 1) {
throw InvalidGenerationSourceError(
Expand Down
5 changes: 4 additions & 1 deletion json_serializable/lib/src/utils.dart
Expand Up @@ -66,7 +66,7 @@ JsonSerializable _valueForAnnotation(ConstantReader reader) => JsonSerializable(
includeIfNull: reader.read('includeIfNull').literalValue as bool?,
);

/// Returns a [JsonSerializable] with values from the [JsonSerializable]
/// Returns a [ClassConfig] with values from the [JsonSerializable]
/// instance represented by [reader].
///
/// For fields that are not defined in [JsonSerializable] or `null` in [reader],
Expand All @@ -93,6 +93,8 @@ ClassConfig mergeConfig(
.where((element) => element.hasDefaultValue)
.map((e) => MapEntry(e.name, e.defaultValueCode!)));

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

return ClassConfig(
anyMap: annotation.anyMap ?? config.anyMap,
checked: annotation.checked ?? config.checked,
Expand All @@ -109,6 +111,7 @@ ClassConfig mergeConfig(
ignoreUnannotated: annotation.ignoreUnannotated ?? config.ignoreUnannotated,
includeIfNull: annotation.includeIfNull ?? config.includeIfNull,
ctorParamDefaults: paramDefaultValueMap,
converters: converters.isNull ? const [] : converters.listValue,
);
}

Expand Down
4 changes: 4 additions & 0 deletions json_serializable/pubspec.yaml
Expand Up @@ -36,3 +36,7 @@ dev_dependencies:
test_descriptor: ^2.0.0
test_process: ^2.0.0
yaml: ^3.0.0

dependency_overrides:
kevmoo marked this conversation as resolved.
Show resolved Hide resolved
json_annotation:
path: ../json_annotation