1

I am using Flutter Freezed package which in turn uses Dart json_serialize package.

I have this Dart enhanced enum with 2 fields:

enum WorkingMode {
  mix(type: 1, name: "mix"),
  parallel(type: 2, name: "parallel"),
  sequential(type: 3, name: "sequential"),
  relay(type: 4, name: "relay"),
  free(type: 5, name: "free"),
  associative(type: 6, name: "associative");

  final int type;
  final String name;

  const WorkingMode({
    required this.type,
    required this.name,
  });
}

Which is referenced in this @freezed class:

@freezed
class Test with _$Test {
  const factory Test({
    required List<WorkingMode> workingModes,
  }) = _Test;

  factory Test.fromJson(Map<String, Object?> json) => _$TestFromJson(json);
}

And this is the code generated for the fromJson() method:

_$_Test _$$_TestFromJson(Map<String, dynamic> json) => _$_Test(
      workingModes: (json['workingModes'] as List<dynamic>)
          .map((e) => $enumDecode(_$WorkingModeEnumMap, e))
          .toList(),
    );

const _$WorkingModeEnumMap = {
  WorkingMode.mix: 'mix',
};

This code expects to decode the final value only based in one particular field, in this case the name field. I mean it works if the JSON received is like this:

{
   "workingModes":[
        "parallel",
        "sequential",
        "mix",
        "relay",
        "free",
        "associative"
   ]
}

But the problem I have is that I am receiving the JSON with all the fields in the enum from a Java REST API like this:

{
   "workingModes":[
      {
         "type":2,
         "name":"parallel"
      },
      {
         "type":3,
         "name":"sequential"
      },
      {
         "type":1,
         "name":"mix"
      },
      {
         "type":4,
         "name":"relay"
      },
      {
         "type":5,
         "name":"free"
      },
      {
         "type":6,
         "name":"associative"
      }
   ]
}

I have tried to use the @JsonValue and @JsonEnum(valueField: 'XXX ) annotations but it seems you can specify another different field, but not several fields at the same time.

At this time I have resolved this by coding my own fromJson/toJson methods through @JsonKey:

@freezed
class Test with _$Test {
  const factory Test({
    @JsonKey(
      fromJson: fromJsonWorkingModes,
      toJson: toJsonWorkingModes,
    )
    required List<WorkingMode> workingModes,
  }) = _Test;

  factory Test.fromJson(Map<String, Object?> json) => _$TestFromJson(json);
}

List<WorkingMode> fromJsonWorkingModes(List<dynamic> rawWorkingModes) {
  return rawWorkingModes
      .map((rawWorkingMode) => WorkingMode.values
          .firstWhere((element) => element.type == rawWorkingMode['type']))
      .toList();
}

List<dynamic> toJsonWorkingModes(List<WorkingMode> workingModes) {
  List<dynamic> finalWorkingModes = [];
  for (WorkingMode workingMode in workingModes) {
    var data = {'type': workingMode.type, 'name': workingMode.name};
    finalWorkingModes.add(data);
  }
  return finalWorkingModes;
}

But I was wondering if this could be done in a more direct form. Of course another possibility would be getting rid of the enhanced enum and transform it into a plain class also annotated with @freezed but that way I'd lose all the benefits of the enum.

Thanks.

rocotocloc
  • 418
  • 6
  • 19

2 Answers2

0

I'm not in a place where I can test this at the moment, but it seems like the intended way is to use the JsonEnum decorator to specify which field you want to represent the serialized enum.

@JsonEnum(valueField: 'name')
enum WorkingMode {
  mix(type: 1, name: "mix"),
  parallel(type: 2, name: "parallel"),
  sequential(type: 3, name: "sequential"),
  relay(type: 4, name: "relay"),
  free(type: 5, name: "free"),
  associative(type: 6, name: "associative");

  final int type;
  final String name;

  const WorkingMode({
    required this.type,
    required this.name,
  });
}
Abion47
  • 22,211
  • 4
  • 65
  • 88
  • Not really @Abion47, I am receiving the enum from the API like a JSON with all the fields: {'workingModes': [{"type":2, "name":"parallel"}]}. The annotation you mention lets you work with one single field, either the 'name' or the 'type' field. It would work if the JSON were either {'workingModes': ["parallel"]} or {'workingModes': [2]}. But what if I receive all the fields as a JSON object?. Just for your reference, the Java API is using JACKSON library with the @JsonFormat(shape = JsonFormat.Shape.OBJECT) annotation to serialize the enum: https://www.baeldung.com/jackson-serialize-enums – rocotocloc Jul 28 '23 at 09:36
  • @rocotocloc Then it sounds like what you're working with is not actually an enum in the strictest sense and you'd be better off implementing this as a sealed class. – Abion47 Jul 28 '23 at 17:48
  • I completely agree, that's the main issue. I have just posted an answer with a sealed class. Thanks again for your help @Abion47 – rocotocloc Jul 31 '23 at 10:59
0

As @Abion47 has pointed out the source problem is I am not receiving a strict enum object from the REST API point.

There are 2 possible solutions (at least) to the problem.

The first one is using an enum with custom fromJson/toJson fields through @JsonKey as already described in the original post.

The other solution is using a sealed class. I have created the sealed class like the following. Notice the use of @Freezed(unionKey: 'name') and @FreezedUnionValue to fit the names received in the JSON for my particular case.

@Freezed(unionKey: 'name')
sealed class WorkingModeSealed with _$WorkingModeSealed {
  @FreezedUnionValue('property.working.mode.mix')
  factory WorkingModeSealed.mix(int type, String name) = MixMode;
  @FreezedUnionValue('property.working.mode.parallel')
  factory WorkingModeSealed.parallel(int type, String name) = ParallelMode;
  @FreezedUnionValue('property.working.mode.sequential')
  factory WorkingModeSealed.sequential(int type, String name) = SequentialMode;
  @FreezedUnionValue('property.working.mode.relay')
  factory WorkingModeSealed.relay(int type, String name) = RelayMode;
  @FreezedUnionValue('property.working.mode.free')
  factory WorkingModeSealed.free(int type, String name) = FreeMode;
  @FreezedUnionValue('property.working.mode.associative')
  factory WorkingModeSealed.associative(int type, String name) =
      AssociativeMode;

  factory WorkingModeSealed.fromJson(Map<String, Object?> json) =>
      _$WorkingModeSealedFromJson(json);
}

And the base class to deserialize the JSON like this:

@freezed
class Test with _$Test {
  const factory Test({
    required List<WorkingModeSealed> workingModesSealed,
  }) = _Test;

  factory Test.fromJson(Map<String, Object?> json) => _$TestFromJson(json);
}
rocotocloc
  • 418
  • 6
  • 19