json_serializable - Add a generic field to a freezed/json_serializable class

7.3k views Asked by At

How do I make a Freezed object take a generic type? I want to do this:

import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:vepo/src/entity_types/option_entity.dart';

part 'vegan_item_tag.freezed.dart';
part 'vegan_item_tag.g.dart';

@freezed
abstract class VeganItemTag<T>
    with _$VeganItemTag<T>
    implements OptionEntity<T> {
  const factory VeganItemTag({int? iconCodePoint, T? id, String? name}) =
      _VeganItemTag;

  const VeganItemTag._();

  factory VeganItemTag.fromJson(Map<String, dynamic> json) =>
      _$VeganItemTagFromJson(json);
}

I've tried using @With.fromString('AdministrativeArea<House>') from the docs but can't apply it correctly to this class.

One of the errors:

lib/src/common/enums/tags/common/vegan_item_tag.freezed.dart:142:32: Error: Too few positional arguments: 2 required, 1 given.
$$_VeganItemTagFromJson(json);

Think I might be on the right track with this, but it no longer generates a vegan_item_tag.g.dart file:

@freezed
abstract class VeganItemTag<T>
    with _$VeganItemTag<T>
    implements OptionEntity<T> {
  const factory VeganItemTag(
      {required int iconCodePoint,
      required T id,
      required String name}) = _VeganItemTag;

  const VeganItemTag._();

  factory VeganItemTag.fromJson(
    Map<String, Object?> json,
    T Function(Object?) fromJsonT,
  ) => VeganItemTag(
      iconCodePoint: json['iconCodePoint'] as int,
      id: fromJsonT(json['id']),
      name: json['name'] as String,
    );
}
2

There are 2 answers

0
Cavitedev On BEST ANSWER

There are several solutions to this problem. But in all of them you need to explicitly convert your classes to a generic type Firebase can handle such as String or Map<dynamic, String>.

The 3 ways to implement such behavior are:

FromJson ToJson

This is messier to maintain than JsonConverters on complex scenarios so I would discard this option for your solution.

JsonConverters

It works for automatizing conversions of specific classes or abstract classes through inheritance, but from generic types with different data to store it may not be what you require. If you are always saving the same values from the generic type T you may try to use this solution through implemented abstract classes.

GenericArgumentFactories

This is what you are actually asking about. Working with genericArgumentFactories on json_serializable and Freezed is not easy and I found a bug on Freezed package meanwhile.

But I managed to get this code working which is the actual solution .

@freezed
@JsonSerializable(genericArgumentFactories: true)
class VeganItemTagV2<T> with _$VeganItemTagV2<T> {
  const VeganItemTagV2._();

  const factory VeganItemTagV2({
    required int iconCodePoint,
    required T id,
    required String name,
  }) = _VeganItemTag<T>;

  //It only works with block bodies and not with expression bodies
  //I don't know why
  factory VeganItemTagV2.fromJson(
      Map<String, dynamic> json, T Function(Object? json) fromJsonT) {
    return _$VeganItemTagV2FromJson<T>(json, fromJsonT);
  }

  Map<String, dynamic> toJson(Object Function(T value) toJsonT) {
    return _$VeganItemTagV2ToJson<T>(this, toJsonT);
  }
}

This adds the converters on the toJson and fromJson methods to be used depending on the generic type.

NOTE. These methods can't be expressions for some bug as it doesn't compile but it works with block bodies. Freezed does not oficcially support it so you may consider creating this class without Freezed package.

This is a example with an encapsulated class of a String and a test class to see how it works:

class VeganId {
  final String id;

  VeganId(this.id);

  String itemId() {
    return id;
  }

  @override
  String toString() {
    return 'VeganId{id: $id}';
  }

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is VeganId && runtimeType == other.runtimeType && id == other.id;

  @override
  int get hashCode => id.hashCode;
}

And the test which works fine

  test('veganItemV2 from and toJson', () {
    final dto = VeganItemTagV2<VeganId>(
      iconCodePoint: 1,
      id: VeganId("veganID"),
      name: "name",
    );

    final Map<String, dynamic> actualToJson = dto.toJson((id) => id.itemId());

    expect(actualToJson, {"iconCodePoint": 1, "id": "veganID", "name": "name"});

    final VeganItemTagV2 actualFromJson = VeganItemTagV2<VeganId>.fromJson(
      actualToJson,
      (json) =>
        VeganId(json as String),
    );

    expect(actualFromJson, dto);
  });
1
kevinkwee On

Your last code doesn't generate the vegan_item_tag.g.dart because you wrote a wrong code in the VeganItemTag.fromJson factory. Edit it to be something like this:

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

Or:

factory VeganItemTag.fromJson(Map<String, Object?> json) =>
    _$VeganItemTagFromJson(json);

And then re-run the flutter pub run build_runner build --delete-conflicting-outputs command.