Skip to content

More control over which fields should be added to fromJson and toJson #1178

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

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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
6 changes: 4 additions & 2 deletions json_annotation/lib/src/json_key.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class JsonKey {
///
/// If `null` (the default) or `false`, the field will be considered for
/// serialization.
final bool? ignore;
final IncludeWith? includeWith;

/// Whether the generator should include fields with `null` values in the
/// serialized output.
Expand Down Expand Up @@ -126,7 +126,7 @@ class JsonKey {
this.defaultValue,
this.disallowNullValue,
this.fromJson,
this.ignore,
this.includeWith,
this.includeIfNull,
this.name,
this.readValue,
Expand All @@ -142,3 +142,5 @@ class JsonKey {
}

enum _NullAsDefault { value }

enum IncludeWith { both, toJson, fromJson, ignore, legacy }
Copy link
Author

@joeldomke joeldomke Jul 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a fifth option, currently called legacy. Fields that use this option should behave as fields that previously used ignore: false. Currently this is the default option.

2 changes: 1 addition & 1 deletion json_annotation/lib/src/json_serializable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ class JsonSerializable {
/// @myCustomAnnotation
/// class Another {...}
/// ```
@JsonKey(ignore: true)
@JsonKey(includeWith: IncludeWith.ignore)
final List<JsonConverter>? converters;

/// Creates a new [JsonSerializable] instance.
Expand Down
28 changes: 22 additions & 6 deletions json_serializable/lib/src/generator_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import 'package:analyzer/dart/element/element.dart';
import 'package:build/build.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:source_gen/source_gen.dart';

import 'decode_helper.dart';
Expand Down Expand Up @@ -61,14 +62,16 @@ class GeneratorHelper extends HelperCore with EncodeHelper, DecodeHelper {
final accessibleFields = sortedFields.fold<Map<String, FieldElement>>(
<String, FieldElement>{},
(map, field) {
final jsonKey = jsonKeyFor(field);
if (!field.isPublic) {
unavailableReasons[field.name] = 'It is assigned to a private field.';
} else if (field.getter == null) {
} else if (field.getter == null &&
jsonKey.includeWith == IncludeWith.legacy) {
assert(field.setter != null);
unavailableReasons[field.name] =
'Setter-only properties are not supported.';
log.warning('Setters are ignored: ${element.name}.${field.name}');
} else if (jsonKeyFor(field).ignore) {
} else if (jsonKey.includeWith == IncludeWith.ignore) {
unavailableReasons[field.name] =
'It is assigned to an ignored field.';
} else {
Expand All @@ -82,11 +85,19 @@ class GeneratorHelper extends HelperCore with EncodeHelper, DecodeHelper {

var accessibleFieldSet = accessibleFields.values.toSet();
if (config.createFactory) {
final createResult = createFactory(accessibleFields, unavailableReasons);
final createResult = createFactory(Map.from(accessibleFields)
..removeWhere((_, field) =>
!{
IncludeWith.both,
IncludeWith.fromJson,
IncludeWith.legacy
}.contains(jsonKeyFor(field).includeWith)), unavailableReasons);
yield createResult.output;

accessibleFieldSet = accessibleFields.entries
.where((e) => createResult.usedFields.contains(e.key))
.where((e) =>
createResult.usedFields.contains(e.key) ||
jsonKeyFor(e.value).includeWith != IncludeWith.legacy)
.map((e) => e.value)
.toSet();
}
Expand All @@ -113,8 +124,13 @@ class GeneratorHelper extends HelperCore with EncodeHelper, DecodeHelper {
}

if (config.createToJson) {
yield* createToJson(accessibleFieldSet);
}
yield* createToJson(accessibleFieldSet.where((field) =>
{
IncludeWith.both,
IncludeWith.toJson,
IncludeWith.legacy
}.contains(jsonKeyFor(field).includeWith)).toSet());
}

yield* _addedMembers;
}
Expand Down
13 changes: 9 additions & 4 deletions json_serializable/lib/src/json_key_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:analyzer/dart/constant/value.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:build/build.dart';
import 'package:collection/collection.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:source_gen/source_gen.dart';
import 'package:source_helper/source_helper.dart';
Expand Down Expand Up @@ -34,7 +35,8 @@ KeyConfig _from(FieldElement element, ClassConfig classAnnotation) {
classAnnotation,
element,
defaultValue: ctorParamDefault,
ignore: classAnnotation.ignoreUnannotated,
includeWith:
classAnnotation.ignoreUnannotated ? IncludeWith.ignore : null,
);
}

Expand Down Expand Up @@ -236,12 +238,15 @@ KeyConfig _from(FieldElement element, ClassConfig classAnnotation) {
readValue.objectValue.toFunctionValue()!.qualifiedName;
}

final includeWithAnnotationValue = _annotationValue('includeWith');
final includeWith = IncludeWith.values
.firstWhereOrNull((v) => v.toString() == includeWithAnnotationValue);
return _populateJsonKey(
classAnnotation,
element,
defaultValue: defaultValue ?? ctorParamDefault,
disallowNullValue: obj.read('disallowNullValue').literalValue as bool?,
ignore: obj.read('ignore').literalValue as bool?,
includeWith: includeWith,
includeIfNull: obj.read('includeIfNull').literalValue as bool?,
name: obj.read('name').literalValue as String?,
readValueFunctionName: readValueFunctionName,
Expand All @@ -256,7 +261,7 @@ KeyConfig _populateJsonKey(
FieldElement element, {
required String? defaultValue,
bool? disallowNullValue,
bool? ignore,
IncludeWith? includeWith,
bool? includeIfNull,
String? name,
String? readValueFunctionName,
Expand All @@ -275,7 +280,7 @@ KeyConfig _populateJsonKey(
return KeyConfig(
defaultValue: defaultValue,
disallowNullValue: disallowNullValue ?? false,
ignore: ignore ?? false,
includeWith: includeWith ?? IncludeWith.legacy,
includeIfNull: _includeIfNull(
includeIfNull, disallowNullValue, classAnnotation.includeIfNull),
name: name ?? encodedFieldName(classAnnotation.fieldRename, element.name),
Expand Down
4 changes: 2 additions & 2 deletions json_serializable/lib/src/type_helpers/config_types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class KeyConfig {

final bool disallowNullValue;

final bool ignore;
final IncludeWith includeWith;

final bool includeIfNull;

Expand All @@ -26,7 +26,7 @@ class KeyConfig {
KeyConfig({
required this.defaultValue,
required this.disallowNullValue,
required this.ignore,
required this.includeWith,
required this.includeIfNull,
required this.name,
required this.readValueFunctionName,
Expand Down
2 changes: 1 addition & 1 deletion json_serializable/test/integration/field_map_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class Model {
@JsonKey(name: 'LAST_NAME')
final String lastName;

@JsonKey(ignore: true)
@JsonKey(includeWith: IncludeWith.ignore)
final String? ignoredName;

String get fullName => '$firstName $lastName';
Expand Down
4 changes: 2 additions & 2 deletions json_serializable/test/integration/json_test_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ class Order {
)
StatusCode? statusCode;

@JsonKey(ignore: true)
@JsonKey(includeWith: IncludeWith.ignore)
String get platformValue => platform!.description;

set platformValue(String value) {
Expand All @@ -78,7 +78,7 @@ class Order {
// Ignored getter without value set in ctor
int get price => items!.fold(0, (total, item) => item.price! + total);

@JsonKey(ignore: true)
@JsonKey(includeWith: IncludeWith.ignore)
bool? shouldBeCached;

Order.custom(this.category, [Iterable<Item>? items])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ class Order {
)
StatusCode? statusCode;

@JsonKey(ignore: true)
@JsonKey(includeWith: IncludeWith.ignore)
String get platformValue => platform!.description;

set platformValue(String value) {
Expand All @@ -80,7 +80,7 @@ class Order {
// Ignored getter without value set in ctor
int get price => items!.fold(0, (total, item) => item.price! + total);

@JsonKey(ignore: true)
@JsonKey(includeWith: IncludeWith.ignore)
bool? shouldBeCached;

Order.custom(this.category, [Iterable<Item>? items])
Expand Down
3 changes: 3 additions & 0 deletions json_serializable/test/json_serializable_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ const _expectedAnnotatedTests = {
'IgnoreUnannotated',
'IncludeIfNullDisallowNullClass',
'IncludeIfNullOverride',
'IncludeWithFromJson',
'IncludeWithToJson',
'IncludeWithToJsonNoFactory',
'InvalidChildClassFromJson',
'InvalidChildClassFromJson2',
'InvalidChildClassFromJson3',
Expand Down
13 changes: 7 additions & 6 deletions json_serializable/test/src/_json_serializable_test_input.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ part 'core_subclass_type_input.dart';
part 'default_value_input.dart';
part 'field_namer_input.dart';
part 'generic_test_input.dart';
part 'include_with_test_input.dart';
part 'inheritance_test_input.dart';
part 'json_converter_test_input.dart';
part 'map_key_variety_test_input.dart';
Expand Down Expand Up @@ -288,17 +289,17 @@ class DupeKeys {
@ShouldGenerate(r'''
Map<String, dynamic> _$IgnoredFieldClassToJson(IgnoredFieldClass instance) =>
<String, dynamic>{
'ignoredFalseField': instance.ignoredFalseField,
'ignoredBothField': instance.ignoredBothField,
'ignoredNullField': instance.ignoredNullField,
};
''')
@JsonSerializable(createFactory: false)
class IgnoredFieldClass {
@JsonKey(ignore: true)
late int ignoredTrueField;
@JsonKey(includeWith: IncludeWith.ignore)
late int ignoredIgnoreField;

@JsonKey(ignore: false)
late int ignoredFalseField;
@JsonKey(includeWith: IncludeWith.both)
late int ignoredBothField;

late int ignoredNullField;
}
Expand All @@ -310,7 +311,7 @@ class IgnoredFieldClass {
)
@JsonSerializable()
class IgnoredFieldCtorClass {
@JsonKey(ignore: true)
@JsonKey(includeWith: IncludeWith.ignore)
int ignoredTrueField;

IgnoredFieldCtorClass(this.ignoredTrueField);
Expand Down
90 changes: 90 additions & 0 deletions json_serializable/test/src/include_with_test_input.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
// 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.

part of '_json_serializable_test_input.dart';

@ShouldGenerate(r'''
IncludeWithToJson<T, S> _$IncludeWithToJsonFromJson<T extends num, S>(
Map<String, dynamic> json) =>
IncludeWithToJson<T, S>()..someString = json['someString'] as String;

Map<String, dynamic> _$IncludeWithToJsonToJson<T extends num, S>(
IncludeWithToJson<T, S> instance) =>
<String, dynamic>{
'someString': instance.someString,
'getterIncluded': instance.getterIncluded,
'finalFieldIncluded': instance.finalFieldIncluded,
};
''')
@JsonSerializable()
class IncludeWithToJson<T extends num, S> {
late String someString;

String get getterExcluded => someString.toLowerCase();

@JsonKey(includeWith: IncludeWith.toJson)
String get getterIncluded => someString.toLowerCase();

final finalFieldExcluded = 'finalFieldExcluded';

@JsonKey(includeWith: IncludeWith.toJson)
final finalFieldIncluded = 'finalFieldIncluded';

IncludeWithToJson();
}

@ShouldGenerate(r'''
Map<String, dynamic> _$IncludeWithToJsonNoFactoryToJson<T extends num, S>(
IncludeWithToJsonNoFactory<T, S> instance) =>
<String, dynamic>{
'someString': instance.someString,
'getterIncludeWithNull': instance.getterIncludeWithNull,
'getterIncludeWithToJson': instance.getterIncludeWithToJson,
'getterIncludeWithBoth': instance.getterIncludeWithBoth,
};
''')
@JsonSerializable(createFactory: false)
class IncludeWithToJsonNoFactory<T extends num, S> {
late String someString;

String get getterIncludeWithNull => someString.toLowerCase();

@JsonKey(includeWith: IncludeWith.ignore)
String get getterIncludeWithIgnore => someString.toLowerCase();

@JsonKey(includeWith: IncludeWith.toJson)
String get getterIncludeWithToJson => someString.toLowerCase();

@JsonKey(includeWith: IncludeWith.fromJson)
String get getterIncludeWithFromJson => someString.toLowerCase();

@JsonKey(includeWith: IncludeWith.both)
String get getterIncludeWithBoth => someString.toLowerCase();

IncludeWithToJsonNoFactory();
}

@ShouldGenerate(r'''
IncludeWithFromJson<T, S> _$IncludeWithFromJsonFromJson<T extends num, S>(
Map<String, dynamic> json) =>
IncludeWithFromJson<T, S>()
..someString = json['someString'] as String
..setter = json['setter'] as String;

Map<String, dynamic> _$IncludeWithFromJsonToJson<T extends num, S>(
IncludeWithFromJson<T, S> instance) =>
<String, dynamic>{};
''')
@JsonSerializable()
class IncludeWithFromJson<T extends num, S> {
@JsonKey(includeWith: IncludeWith.fromJson)
late String someString;

@JsonKey(includeWith: IncludeWith.fromJson)
String get setter => someString;

set setter(String value) => someString = value;
Comment on lines +84 to +87
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the JsonKey annotation can't be added to setters, we still need a getter.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it would be possible to also support fields that only have setters? @kevmoo would you consider this PR, if I implemented this, or are there other issues with this PR or just limited time/resources from your side?


IncludeWithFromJson();
}
2 changes: 1 addition & 1 deletion json_serializable/test/src/inheritance_test_input.dart
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ class SubTypeWithAnnotatedFieldOverrideImplements implements SuperType {
@override
int? superReadWriteField;

@JsonKey(ignore: true)
@JsonKey(includeWith: IncludeWith.ignore)
@override
int get priceHalf => 42;

Expand Down