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.
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.
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']),
);
In short, use the following latest implementation of enum serialisation:
fromJson -> YourEnum.values.byName("property")
toJson -> YourEnum.property.name
toJson/fromJson on you enumSimply 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);
}
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.
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;
}
}
}