Skip to content
Open
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
81 changes: 68 additions & 13 deletions json_serializable/lib/src/type_helpers/json_converter_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:json_annotation/json_annotation.dart';
import 'package:source_gen/source_gen.dart';
import 'package:source_helper/source_helper.dart';

import '../json_literal_generator.dart';
import '../lambda_result.dart';
import '../type_helper.dart';
import '../utils.dart';
Expand Down Expand Up @@ -115,18 +116,22 @@ class _JsonConvertData {
String className,
String accessor,
this.jsonType,
this.fieldType,
) : accessString = 'const $className${_withAccessor(accessor)}()',
isGeneric = false;
this.fieldType, [
String constructorArgs = '',
]) : accessString =
'const $className${_withAccessor(accessor)}($constructorArgs)',
isGeneric = false;

_JsonConvertData.genericClass(
String className,
String genericTypeArg,
String accessor,
this.jsonType,
this.fieldType,
) : accessString = '$className<$genericTypeArg>${_withAccessor(accessor)}()',
isGeneric = true;
this.fieldType, [
String constructorArgs = '',
]) : accessString =
'$className<$genericTypeArg>${_withAccessor(accessor)}($constructorArgs)',
isGeneric = true;

_JsonConvertData.propertyAccess(
this.accessString,
Expand Down Expand Up @@ -234,13 +239,7 @@ _JsonConvertData? _typeConverterFrom(

final reviver = ConstantReader(match.annotation).revive();

if (reviver.namedArguments.isNotEmpty ||
reviver.positionalArguments.isNotEmpty) {
throw InvalidGenerationSourceError(
'Generators with constructor arguments are not supported.',
element: match.elementAnnotation?.element,
);
}
final args = _parseArguments(reviver);

if (match.genericTypeArg != null) {
return _JsonConvertData.genericClass(
Expand All @@ -249,6 +248,7 @@ _JsonConvertData? _typeConverterFrom(
reviver.accessor,
match.jsonType,
match.fieldType,
args,
);
}

Expand All @@ -257,6 +257,61 @@ _JsonConvertData? _typeConverterFrom(
reviver.accessor,
match.jsonType,
match.fieldType,
args,
);
}

String _parseArguments(Revivable reviver) {
final args = <String>[];
for (final arg in reviver.positionalArguments) {
args.add(_dartObjectToString(arg));
}
for (final entry in reviver.namedArguments.entries) {
args.add('${entry.key}: ${_dartObjectToString(entry.value)}');
}
return args.join(', ');
}

String _dartObjectToString(DartObject object) {
final reader = ConstantReader(object);

if (reader.isNull) {
return 'null';
}

if (reader.isBool || reader.isInt || reader.isDouble || reader.isString) {
return jsonLiteralAsDart(reader.literalValue);
}

if (reader.isList) {
final list = reader.listValue.map(_dartObjectToString).join(', ');
return '[$list]';
}

if (reader.isMap) {
final map = reader.mapValue.entries
.map((e) {
return '${_dartObjectToString(e.key!)}: ${_dartObjectToString(e.value!)}';
})
.join(', ');
return '{$map}';
}

if (reader.isSet) {
final set = reader.setValue.map(_dartObjectToString).join(', ');
return '{$set}';
}

final type = object.type;
if (type is InterfaceType && type.element is EnumElement) {
final enumElement = type.element as EnumElement;
final index = object.getField('index')!.toIntValue()!;
return '${type.element.name}.${enumElement.constants[index].name}';
}

throw InvalidGenerationSourceError(
'Unsupported argument type for JsonConverter: ${object.type}',
element: object.type?.element,
);
}

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 @@ -94,8 +94,11 @@ const _expectedAnnotatedTests = {
'JsonConverterCtorParams',
'JsonConverterDuplicateAnnotations',
'JsonConverterNamedCtor',
'JsonConverterListParams',
'JsonConverterNamedParams',
'JsonConverterNullableToNonNullable',
'JsonConverterOnGetter',
'JsonConverterStringParams',
'JsonConverterWithBadTypeArg',
'JsonValueValid',
'JsonValueWithBool',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ part 'extends_jsonkey_override.dart';
part 'generic_test_input.dart';
part 'inheritance_test_input.dart';
part 'json_converter_test_input.dart';
part 'json_converter_params_test_input.dart';
part 'map_key_variety_test_input.dart';
part 'setter_test_input.dart';
part 'to_from_json_test_input.dart';
Expand Down
137 changes: 137 additions & 0 deletions json_serializable/test/src/json_converter_params_test_input.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// 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.

// @dart=3.8

// ignore_for_file: inference_failure_on_instance_creation

part of '_json_serializable_test_input.dart';

@ShouldGenerate(r'''
JsonConverterCtorParams _$JsonConverterCtorParamsFromJson(
Map<String, dynamic> json,
) => JsonConverterCtorParams()
..value = const _ConverterWithCtorParams(
42,
).fromJson((json['value'] as num).toInt());

Map<String, dynamic> _$JsonConverterCtorParamsToJson(
JsonConverterCtorParams instance,
) => <String, dynamic>{
'value': const _ConverterWithCtorParams(42).toJson(instance.value),
};
''')
@JsonSerializable()
@_ConverterWithCtorParams(42)
class JsonConverterCtorParams {
late Duration value;
}

class _ConverterWithCtorParams implements JsonConverter<Duration, int> {
final int param;

const _ConverterWithCtorParams(this.param);

@override
Duration fromJson(int json) => throw UnimplementedError();

@override
int toJson(Duration object) => 0;
}

@ShouldGenerate(r'''
JsonConverterStringParams _$JsonConverterStringParamsFromJson(
Map<String, dynamic> json,
) => JsonConverterStringParams()
..value = const _ConverterWithStringParams(
'cool',
).fromJson((json['value'] as num).toInt());

Map<String, dynamic> _$JsonConverterStringParamsToJson(
JsonConverterStringParams instance,
) => <String, dynamic>{
'value': const _ConverterWithStringParams('cool').toJson(instance.value),
};
''')
@JsonSerializable()
@_ConverterWithStringParams('cool')
class JsonConverterStringParams {
late Duration value;
}

class _ConverterWithStringParams implements JsonConverter<Duration, int> {
final String param;

const _ConverterWithStringParams(this.param);

@override
Duration fromJson(int json) => throw UnimplementedError();

@override
int toJson(Duration object) => 0;
}

@ShouldGenerate(r'''
JsonConverterListParams _$JsonConverterListParamsFromJson(
Map<String, dynamic> json,
) => JsonConverterListParams()
..value = const _ConverterWithListParams([
42,
]).fromJson((json['value'] as num).toInt());

Map<String, dynamic> _$JsonConverterListParamsToJson(
JsonConverterListParams instance,
) => <String, dynamic>{
'value': const _ConverterWithListParams([42]).toJson(instance.value),
};
''')
@JsonSerializable()
@_ConverterWithListParams([42])
class JsonConverterListParams {
late Duration value;
}

class _ConverterWithListParams implements JsonConverter<Duration, int> {
final List<int> param;

const _ConverterWithListParams(this.param);

@override
Duration fromJson(int json) => throw UnimplementedError();

@override
int toJson(Duration object) => 0;
}

@ShouldGenerate(r'''
JsonConverterNamedParams _$JsonConverterNamedParamsFromJson(
Map<String, dynamic> json,
) => JsonConverterNamedParams()
..value = const _ConverterWithNamedParams(
param: 42,
).fromJson((json['value'] as num).toInt());

Map<String, dynamic> _$JsonConverterNamedParamsToJson(
JsonConverterNamedParams instance,
) => <String, dynamic>{
'value': const _ConverterWithNamedParams(param: 42).toJson(instance.value),
};
''')
@JsonSerializable()
@_ConverterWithNamedParams(param: 42)
class JsonConverterNamedParams {
late Duration value;
}

class _ConverterWithNamedParams implements JsonConverter<Duration, int> {
final int param;

const _ConverterWithNamedParams({required this.param});

@override
Duration fromJson(int json) => throw UnimplementedError();

@override
int toJson(Duration object) => 0;
}

Choose a reason for hiding this comment

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

medium

This is a great addition! As you mentioned in the pull request description, it would be beneficial to verify support for more complex parameter types. The implementation in _dartObjectToString includes logic for Map and Enum types, but there are no corresponding tests.

Adding test cases for converters with Map and Enum parameters would increase confidence in the implementation and prevent future regressions for these scenarios.

Here's an example of what you could add to this file (and remember to add the new class names to _expectedAnnotatedTests):

@ShouldGenerate(r'''
JsonConverterMapParams _$JsonConverterMapParamsFromJson(
  Map<String, dynamic> json,
) =>
    JsonConverterMapParams()
      ..value = const _ConverterWithMapParams({
        'a': 1,
        'b': null,
      }).fromJson((json['value'] as num).toInt());

Map<String, dynamic> _$JsonConverterMapParamsToJson(
  JsonConverterMapParams instance,
) =>
    <String, dynamic>{
      'value': const _ConverterWithMapParams({'a': 1, 'b': null})
          .toJson(instance.value),
    };
''')
@JsonSerializable()
@_ConverterWithMapParams({'a': 1, 'b': null})
class JsonConverterMapParams {
  late Duration value;
}

class _ConverterWithMapParams implements JsonConverter<Duration, int> {
  final Map<String, int?> param;

  const _ConverterWithMapParams(this.param);

  @override
  Duration fromJson(int json) => throw UnimplementedError();

  @override
  int toJson(Duration object) => 0;
}

enum _MyEnum { alpha, beta }

@ShouldGenerate(r'''
JsonConverterEnumParams _$JsonConverterEnumParamsFromJson(
  Map<String, dynamic> json,
) =>
    JsonConverterEnumParams()
      ..value = const _ConverterWithEnumParams(_MyEnum.beta)
          .fromJson((json['value'] as num).toInt());

Map<String, dynamic> _$JsonConverterEnumParamsToJson(
  JsonConverterEnumParams instance,
) =>
    <String, dynamic>{
      'value':
          const _ConverterWithEnumParams(_MyEnum.beta).toJson(instance.value),
    };
''')
@JsonSerializable()
@_ConverterWithEnumParams(_MyEnum.beta)
class JsonConverterEnumParams {
  late Duration value;
}

class _ConverterWithEnumParams implements JsonConverter<Duration, int> {
  final _MyEnum param;

  const _ConverterWithEnumParams(this.param);

  @override
  Duration fromJson(int json) => throw UnimplementedError();

  @override
  int toJson(Duration object) => 0;
}

22 changes: 0 additions & 22 deletions json_serializable/test/src/json_converter_test_input.dart
Original file line number Diff line number Diff line change
Expand Up @@ -152,28 +152,6 @@ class _DurationMillisecondConverter implements JsonConverter<Duration, int> {
int toJson(Duration object) => throw UnimplementedError();
}

@ShouldThrow(
'Generators with constructor arguments are not supported.',
element: 'new',
)
@JsonSerializable()
@_ConverterWithCtorParams(42)
class JsonConverterCtorParams {
late Duration value;
}

class _ConverterWithCtorParams implements JsonConverter<Duration, int> {
final int param;

const _ConverterWithCtorParams(this.param);

@override
Duration fromJson(int json) => throw UnimplementedError();

@override
int toJson(Duration object) => 0;
}

@ShouldGenerate(r'''
Map<String, dynamic> _$JsonConverterOnGetterToJson(
JsonConverterOnGetter instance,
Expand Down