json_serializable.dart icon indicating copy to clipboard operation
json_serializable.dart copied to clipboard

Generics complicate self-referential class structure

Open craiglabenz opened this issue 1 year ago • 6 comments

The following code builds, but see the comment for which critical information has to be removed for said build to succeed:

class Base<T> {
  const Base();
}

class BaseConverter<T> extends JsonConverter<Base, Map<String, Object?>> {
  const BaseConverter();
  @override
  Map<String, Object?> toJson(Base object) => {};
  @override
  Base fromJson(Map<String, Object?> json) => Base();
}

@JsonSerializable()
class Data<T> extends Base<T> {
  Data({
    required this.data,
    required this.name,
  });

  factory Data.fromJson(Map<String, dynamic> json) => _$DataFromJson(json);

  final String name;

  @BaseConverter()
  // Ideally, this would be Base<T>, but defining it so breaks the build
  final Base data;

  Map<String, dynamic> toJson() => _$DataToJson(this);
}

Changing final Base data to final Base<T> data results in this error:

[SEVERE] json_serializable on lib/src/data.dart:

Could not generate `fromJson` code for `data`.
To support the type `Base` you can:
* Use `JsonConverter`
  https://pub.dev/documentation/json_annotation/latest/json_annotation/JsonConverter-class.html
* Use `JsonKey` fields `fromJson` and `toJson`
  https://pub.dev/documentation/json_annotation/latest/json_annotation/JsonKey/fromJson.html
  https://pub.dev/documentation/json_annotation/latest/json_annotation/JsonKey/toJson.html
package:json_serializable_test/src/data.dart:30:17
   ╷
[line number] │   final Base<T> data;

This despite the fact that a JsonConverter is clearly being used.

For posterity, I originally reported this at rrousselGit/freezed#1074, and was (correctly, it seems) directed here.

craiglabenz avatar Apr 22 '24 16:04 craiglabenz

I have same problem :c

i-panov avatar Jun 10 '24 20:06 i-panov

class BaseConverter<T> extends JsonConverter<Base, Map<String, Object?>> {

What happens if you write Base<T> there, instead of Base?

Writing just Base with no arguments is equivalent, IIUC, to writing Base<dynamic>. Which doesn't seem like what you intend. And it makes sense that a converter that produces a Base<dynamic> wouldn't be seen as a match when the field is supposed to hold more specifically a Base<T>.

That said, I won't be surprised if that doesn't fix it. Here's a possibly related issue:

  • #1398

which suggests that the logic for applying a JsonConverter doesn't currently do everything one would hope it would when there are type parameters involved.

gnprice avatar Jul 01 '24 20:07 gnprice

@craiglabenz – is this what you want?

diff --git a/json_serializable/test/generic_files/for_craig.dart b/json_serializable/test/generic_files/for_craig.dart
index 5555d80..bb0011e 100644
--- a/json_serializable/test/generic_files/for_craig.dart
+++ b/json_serializable/test/generic_files/for_craig.dart
@@ -6,12 +6,12 @@ class Base<T> {
   const Base();
 }

-class BaseConverter<T> extends JsonConverter<Base, Map<String, Object?>> {
+class BaseConverter<T> extends JsonConverter<Base<T>, Map<String, Object?>> {
   const BaseConverter();
   @override
   Map<String, Object?> toJson(Base object) => {};
   @override
-  Base fromJson(Map<String, Object?> json) => const Base();
+  Base<T> fromJson(Map<String, Object?> json) => Base<T>();
 }

 @JsonSerializable()
@@ -26,8 +26,7 @@ class Data<T> extends Base<T> {
   final String name;

   @BaseConverter()
-  // Ideally, this would be Base<T>, but defining it so breaks the build
-  final Base data;
+  final Base<T> data;

   Map<String, dynamic> toJson() => _$DataToJson(this);
 }

And instantiate BaseConverter with the right generic parameter?

kevmoo avatar Aug 15 '24 23:08 kevmoo

Looks right, @kevmoo

craiglabenz avatar Sep 03 '24 09:09 craiglabenz

I have encountered the same problem with generics types on class members. Is there a solution. For now I've created several converters for each <T>. For now I have four types, so it's still manageble, but I plan to have many others. I tried to call static method Base<T> fromJson<T>(Map<String, dynamic> json) passed to JsonKey(fromJson: ...) but I get the same error as with generics converter. I there a plan if and when de/serialization of generic class members will be implemented?

edlman avatar May 26 '25 14:05 edlman

No plan, @edlman. This is very much my hobby project. Happy to fix critical issues in main use cases, but this is very specialized/complicated.

PRs welcome!

kevmoo avatar May 27 '25 18:05 kevmoo