How to manage serialize / deserialize an enum property with Dart / Flutter to Firestore?

Viewed 26183

I need to store a Dart object from my Flutter application in Firestore

This object includes an enum property.

What is the best solution to serialize / deserialize this enum property ?

  • As a String

  • As an Int

I do not find any easy solution to do this.

10 Answers

Flutter is able to generate JSON serialization code. The tutorial you can find here. It references the package json_annotation. It contains also support for enum serialization. So all you need, is use this tool and annotate your enum values with @JsonValue.

From the code docs:

An annotation used to specify how a enum value is serialized.

That's basically all. Let me now illustrate with a small example in code. Imagine an enum with vehicles:

import 'package:json_annotation/json_annotation.dart';

enum Vehicle {
  @JsonValue("bike") BIKE,
  @JsonValue("motor-bike") MOTOR_BIKE,
  @JsonValue("car") CAR,
  @JsonValue("truck") TRUCK,
}

Then you can use this enum in one of your model, for example vehilce_owner.dart that looks like this:

import 'package:json_annotation/json_annotation.dart';

part 'vehicle_owner.g.dart';

@JsonSerializable()
class VehicleOwner{
  final String name;
  final Vehicle vehicle;

  VehicleOwner(this.name, this.vehicle);

  factory VehicleOwner.fromJson(Map<String, dynamic> json) =>
      _$VehicleOwnerFromJson(json);
  Map<String, dynamic> toJson() => _$VehicleOwnerToJson(this);
}

This is what you need to provide according to the json generation howto. Now you need to run the builder or watcher to let flutter generate the code:

flutter pub run build_runner build

Then the generated code will look like as seen below. Take a look at the _$VehicleEnumMap that has been generated respecting your @JsonValue annotations:

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'vehicle_owner.dart';

// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************

// more generated code omitted here ....

const _$VehicleEnumMap = {
  Vehicle.BIKE: 'bike',
  Vehicle.MOTOR_BIKE: 'motor-bike',
  Vehicle.CAR: 'car',
  Vehicle.TRUCK: 'truck',
};

Gunter's answer is correct if a little incomplete.

JSON serializable does handle converting Enum's to and from a string, here is some example code of what is generated:

const _$HoursEnumMap = <Hours, dynamic>{
  Hours.FullTime: 'FullTime',
  Hours.PartTime: 'PartTime',
  Hours.Casual: 'Casual',
  Hours.Contract: 'Contract',
  Hours.Other: 'Other'
};

and in return it converts it back with this fairly obtuse function:

T _$enumDecode<T>(Map<T, dynamic> enumValues, dynamic source) {
  if (source == null) {
    throw ArgumentError('A value must be provided. Supported values: '
        '${enumValues.values.join(', ')}');
  }
  return enumValues.entries
      .singleWhere((e) => e.value == source,
          orElse: () => throw ArgumentError(
              '`$source` is not one of the supported values: '
              '${enumValues.values.join(', ')}'))
      .key;
}

I got so sick of this I decided to make a small package to take away the complexity, and it has come in pretty handy for me:

https://pub.dev/packages/enum_to_string

At the very least its unit tested over a copy/paste solution. Any additions or pull requests are welcome.

The way i do it is by just saving the index of the enum.

Lets say you have an enum:

enum Location {
  EARTH,
  MOON,
  MARS,
}

and a class which holds the enum with following methods:

/// Returns a JSON like Map of this User object
  Map<String, dynamic> toJSON() {
    return {
      "name": this.name,
      "location": this.location.index,
    };
  }

  /// Returns [Player] build from a map with informationen
  factory Player.fromJson(Map<String, dynamic> parsedJson) {
    return new Player(
      name: parsedJson['name'],
      location: Location.values.elementAt(
        parsedJson['location'],
      ),
    );
  }

UPDATE

After @JamesAllen answer regarding maintainability I came up with this new solution:

extension LocationExtension on Location {
  String get name => describeEnum(this);
}

Location parseLocation(final String locationName) {
  switch (locationName) {
    case 'earth':
      return Location.earth;
    case 'moon':
      return Location.moon;
    case 'mars':
      return Location.mars;
    default:
      throw Exception('$locationName is not a valid Location');
  }
}

In your toJson/fromJson do this:

/// Returns a JSON like Map of this User object
  Map<String, dynamic> toJSON() {
    return {
      "name": this.name,
      "location": this.location.name,
    };
  }

  /// Returns [Player] build from a map with informationen
  factory Player.fromJson(Map<String, dynamic> parsedJson) {
    return new Player(
      name: parsedJson['name'],
      location: parseLocation(parsedJson['location']),
    );

Update for 2022

In short, use the following latest implementation of enum serialisation:

fromJson -> YourEnum.values.byName("property")

toJson -> YourEnum.property.name

toJson/fromJson on you enum

Simply add those two functions to your enum and you are good to go. Note that you could also simply create those functions in you class.

enum Manufacturer {
  mercedes,
  volkswagen,
  toyota,
  ford;

  String toJson() => name;
  static Manufacturer fromJson(String json) => values.byName(json);
}

Example

class Car {
  final String name;
  final Manufacturer manufacturer;

  Car(this.name, this.manufacturer);

  Map<String, dynamic> toJson() {
    return {
      "name": name,
      "manufacturer": manufacturer.toJson(),
    };
  }

  static Car fromJson(Map<String, dynamic> jsonData) => Car(
        jsonData['name'],
        Manufacturer.fromJson(jsonData['manufacturer']),
      );
}

If anyone uses "Dart Data Class Generator". You need to annotate with // enum comment.

enter image description here

enter image description here

The best way is to use the enums integer value, because it is the easiest to convert from/to int/enum type.

You need to take care that you add new enum values only at the end when you modify the enum, otherwise persisted values will become invalid.

https://pub.dartlang.org/packages/built_value provides code generation for classes and has its own enums and does JSON (de)serialization for you.

https://pub.dartlang.org/packages/json_serializable seems to support Dart enums directly but I haven't used it myself.

Similar to jksevend's apporoach (saving enum Index), I solved it like this, which saves a readable string. Benefit: You can insert new Enum entries between existing entries without breaking load/save!

class Player
{
    String name;
    Gender gender;

    // functions for jsonEncode and jsonDecode!
    Player.fromJson(Map<String, dynamic> json)
      : name = json['name'],
        gender = getGenderEnum(json['gender']);

    Map<String, dynamic> toJson() => {
      'name': name,
      'gender': getGenderText(gender);
    };
}
enum Gender
{
    MALE,
    FEMALE,
    DIVERSE,
}
String getGenderText(Gender gen)
{
    switch(gen)
        case Gender.MALE:
            return "male";
        case Gender.FEMALE:
            return "female";
        case Gender.DIVERSE:
            return "diverse";
}
Gender getGenderEnum(String gen) {
    for (Gender candidate in Gender.values) {
        if (gen == getGenderText(candidate))
            return candidate;
    }
    return Gender.MALE;
}

Since Dart 2.17 has been released you can now extend Enum class.

The trick is to assign a fixed/immutable value (int, string, whatever you prefer) used as the json representation.

enum Color {
  /// The [jsonValue] must not change in time!
  red(10), // Can be numbers
  blue(20),
  green("myGreen"), // Can be strings as well
  gray(40),
  yellow(50);

  final dynamic jsonValue;
  const Color(this.jsonValue);
  static Color fromValue(jsonValue) =>
      Color.values.singleWhere((i) => jsonValue == i.jsonValue);
}

main() {
  var myValue = Color.green.jsonValue;
  var myEnum = Color.fromValue(myValue);
  print(myEnum);
}

This concept and much more is already implemented within my new jsonize 1.4.0 package which allows to easily serialize Enums, DateTime and any of your own classes. This is a simple enum example:

import 'package:jsonize/jsonize.dart';

enum Color with JsonizableEnum {
  red("rd"),
  blue("bl"),
  green("grn"),
  gray("gry"),
  yellow("yl");

  @override
  final dynamic jsonValue;
  const Color(this.jsonValue);
}

void main() {
  // Register your enum
  Jsonize.registerEnum(Color.values);

  Map<String, dynamic> myMap = {
    "my_num": 1,
    "my_str": "Hello!",
    "my_color": Color.green,
  };
  var jsonRep = Jsonize.toJson(myMap);
  var hereIsMyMap = Jsonize.fromJson(jsonRep);
  print(hereIsMyMap);
}

This is an extended example on jsonize capabilities:

import 'package:jsonize/jsonize.dart';

enum Color with JsonizableEnum {
  red("rd"),
  blue("bl"),
  green("grn"),
  gray("gry"),
  yellow("yl");

  @override
  final dynamic jsonValue;
  const Color(this.jsonValue);
}

class MyClass implements Jsonizable<MyClass> {
  String? str;
  MyClass([this.str]);
  factory MyClass.empty() => MyClass();

  // Jsonizable implementation
  @override
  String get jsonClassCode => "mc";
  @override
  dynamic toJson() => str;
  @override
  MyClass? fromJson(value) => MyClass(value);
}

void main() {
  // Register enums and classes
  Jsonize.registerEnum(Color.values);
  Jsonize.registerClass(MyClass.empty());

  Map<String, dynamic> myMap = {
    "my_num": 1,
    "my_str": "Hello!",
    "my_color": Color.green,
    "my_dt": DateTime.now(),
    "my_class": MyClass("here I am!")
  };
  var jsonRep = Jsonize.toJson(myMap);
  var hereIsMyMap = Jsonize.fromJson(jsonRep);
  print(hereIsMyMap);
}

While bootstrapping my first AWS Amplify project I found this interesting method in their own library:

// only to be used internally by amplify-flutter library
T? enumFromString<T>(String? key, List<T> values) =>
    values.firstWhereOrNull((v) => key == enumToString(v));

And it is invoked like this:

Post.fromJson(Map<String, dynamic> json)
      : id = json['id'],
        type = enumFromString<PostCategory>(json['type'], PostCategory.values),
        ... // Other props

I have to admit, far more clever than my custom methods that used switches to read strings and output enums. Now I know I will not be hired by big tech anytime soon...

// JSON deserialization
PostStatusE getPostStatusEnumByString(String type) {
  switch (type) {
    case "draft":
      return PostStatusE.draft;
      break;
      ...

My favourite approach is to use the built_value package. Not only does it allow you to add properties and methods to an enum (which is incredibly useful) you can use annotations to control the serialized value. This means that you can change the name of enum values or change the ordering without your serialisation breaking, which is a problem with a lot of the other answers here.

part 'my_fancy_enum.g.dart';

class MyFancyEnum extends EnumClass {

  // Use wireNumber to serialise to an int, or wireName to serialise to a String
  @BuiltValueEnumConst(wireNumber: 0)
  static const MyFancyEnum timeline = _$timeline;
  @BuiltValueEnumConst(wireNumber: 1)
  static const MyFancyEnum timeOfDay = _$timeOfDay;
  @BuiltValueEnumConst(wireNumber: 2)
  static const MyFancyEnum dayOfWeek = _$dayOfWeek;

  const MyFancyEnum._(String name) : super(name);


  static BuiltSet<MyFancyEnum> get values => _$values;
  static MyFancyEnumvalueOf(String name) => _$valueOf(name);
  static Serializer<MyFancyEnum> get serializer => _$myFancyEnumSerializer;


  String displayName(BuildContext context) {
    switch(this) {
      case timeline: return AppLocalizations.of(context).trends;
      case timeOfDay: return AppLocalizations.of(context).timeOfDayAnalysis;
      case dayOfWeek: return AppLocalizations.of(context).dayOfWeekAnalysis;
      default: return "";
    }
  }

  IconData get iconData {
    switch(this) {
      case timeline: return Icons.timeline;
      case timeOfDay: return Icons.timelapse;
      case dayOfWeek: return Icons.event;
      default: return Icons.timeline;
    }
  }
}
Related