Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
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: 2 additions & 0 deletions checked_yaml/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,7 @@ dev_dependencies:
test_process: ^2.0.0

dependency_overrides:
json_annotation:
path: ../json_annotation
json_serializable:
path: ../json_serializable
36 changes: 36 additions & 0 deletions json_annotation/lib/src/json_serializable.dart
Original file line number Diff line number Diff line change
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';
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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 {
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading