trevorwang / retrofit.dart

retrofit.dart is an dio client generator using source_gen and inspired by Chopper and Retrofit.
https://mings.in/retrofit.dart/
MIT License
1.06k stars 241 forks source link

Wrong generated code with generic type as argument #646

Open woprandi opened 7 months ago

woprandi commented 7 months ago

As specified here https://github.com/trevorwang/retrofit.dart/issues/627#issuecomment-1843458890

@GET('/test')
Future<T> get<T>();

Generate invalid code : final value = T.fromJson(_result.data!);

  @override
  Future<T> get<T>() async {
    const _extra = <String, dynamic>{};
    final queryParameters = <String, dynamic>{};
    final _headers = <String, dynamic>{};
    final Map<String, dynamic>? _data = null;
    final _result =
        await _dio.fetch<Map<String, dynamic>>(_setStreamType<T>(Options(
      method: 'GET',
      headers: _headers,
      extra: _extra,
    )
            .compose(
              _dio.options,
              '/test',
              queryParameters: queryParameters,
              data: _data,
            )
            .copyWith(
                baseUrl: _combineBaseUrls(
              _dio.options.baseUrl,
              baseUrl,
            ))));
    final value = T.fromJson(_result.data!);
    return value;
  }

We also lost info if generic type is restricted

  @GET('/test')
  Future<T> get<T extends String>();

become

  @override
  Future<T> get<T>() async {

but it should be

  @override
  Future<T> get<T extends String>() async {

Tested with 8.0.1 and 8.0.5

TekExplorer commented 7 months ago

Note: retrofit has no way of knowing what to call feomJson on, so it would need to implement something like genericArgumentFactories where we pass in the fromJson to the method

trevorwang commented 7 months ago

There's no reflection for lutter. So, you must tell retrofit the exact type before it generates code for you. Refer ApiResult<t> for an example. @woprandi @ekuleshov

ekuleshov commented 7 months ago

@trevorwang appreciate response. I'm aware of the issue involved with both generics and absence of reflection.

However it is a valid case to have rest api to receive or return a generic argument, when the type is not known at the compilation time. For such types the JsonSerializable can generate special to/fromJson() methods that are taking additional parameters (i.e. genericArgumentFactories option).

Currently retrofit generator is generating an invalid code like T.fromJson(_result.data!) for such classes and it doesn't seem like it allows to pass generic arg factories as code generated by the JsonSerializable is expecting.

It would be really helpful and would allow to deal with the issue without writing a bunch of boilerplate code, if we could specify additional parameters for the retrofit endpoint method, so the consuming code could provide appropriate generic factories and these factories would be wired with the to/fromJson calls generated by retrofit generator.

For example, a rest endpoint declaration could look like this:

  @POST('/api/config/save')
  @Headers({'Content-Type': 'application/json'})
  Future<Config<T>> saveConfig<T>(
          Config<T> config,
          Object? Function(T value) toJson,
          T Function(Object? json) fromJson);

Note that Config<T> class is declared like that and defined the same generic arg factories params in to/fromJson methods.

@JsonSerializable(genericArgumentFactories: true)
class Config<T> {
  ...
  T details;

  factory Config.fromJson(Map<String, dynamic> json, T Function(Object? json) fromJsonT)
       => _$ReportConfigFromJson(json, fromJsonT);

  Map<String, dynamic> toJson(Object? Function(T value) toJsonT) => _$ReportConfigToJson(this, toJsonT);
}
Raviteja11122 commented 7 months ago

Is there any work around for this issue?

kjxbyz commented 1 month ago

Is the following code now supported?

@immutable
@Freezed(genericArgumentFactories: true)

class EnumInfo<T> with _$EnumInfo<T> {
  /// constructor
  const factory EnumInfo({
    ///
    T? code,

    ///
    String? chineseValue,

    ///
    String? englishValue,
  }) = _EnumInfo;

  /// Deserializes the given json into a [EnumInfo].
  factory EnumInfo.fromJson(Map<String, dynamic> json, T Function(Object?) toJsonT) =>
      _$EnumInfoFromJson(json, toJsonT);
}
@RestApi()
abstract class CommonApi {
  @GET("your url")
  Future<List<EnumInfo<int>>?> getTerms();
}