progNewbie
progNewbie

Reputation: 4832

Encode Map with Enum to JSON

I want to encode my Map to a json. It looks like this:

Map<MyEnum, int> map = {type: limit};

Where MyEnum is an enum. A simple json.encode(map) won't work as it does not know how to serialize the enum class I guess.

The error message is:

Unhandled Exception: Converting object to an encodable object failed: _LinkedHashMap len:1

How can I manage to serialize this map to a json?

Upvotes: 1

Views: 3777

Answers (5)

&#199;ağlar YILMAZ
&#199;ağlar YILMAZ

Reputation: 161

  1. Firstly define enum and value extension
enum OrderState { pending, filled, triggered, cancelled }

extension OrderStateExt on OrderState {
  String get value {
    switch (this) {
      case OrderState.pending:
        return "PENDING";
      case OrderState.filled:
        return "FILLED";
      case OrderState.triggered:
        return "TRIGGERED";
      case OrderState.cancelled:
        return "CANCELLED";
    }
  }
}
  1. model class
class OrderRequest {
 OrderState state;
 OrderRequest({required this.state});

Map<String, dynamic> toMap() {
    return {     
      'state': state.value,    
    };
  }
}

String toJson() => json.encode(toMap());

factory OrderRequest.fromMap(Map<String, dynamic> map) {
    return OrderRequest (     
      state: OrderState.values
          .where((e) => e.value == map['state']).first      
    );
  } 
 factory OrderRequest.fromJson(String source) =>
      OrderRequest.fromMap(json.decode(source));

Upvotes: 0

Moacir Schmidt
Moacir Schmidt

Reputation: 945

you will need a function to convert string to enum:

T enumFromString<T>(List<T> values, String value) {
  return values.firstWhere((v) => v.toString().split('.')[1] == value, orElse: () => null);
}

your enum is

enum MyEnum {type, destype};

suppose it is used as map inside a class to serialize and deserialize:

class MyClass {
  Map<MyEnum, int> myProperty = {type: 1}; 

  // serialize
  Map<String, dynamic> toJson() {
    return {
      'myProperty': myProperty.map((key, value) => MapEntry(key.name, value)),
    }

  // deserialize
  MyClass.fromJson(Map<String, dynamic> json) {
    myProperty=
        json['myProperty'].map((k, v) => MapEntry(enumFromString<MyEnum>(MyEnum.values, k), v)).cast<MyEnum, int>();
   }
}

Upvotes: 0

julemand101
julemand101

Reputation: 31309

This is really not a solution I would recommend but I ended up doing it mostly for "fun". I don't guarantee anything about the solution besides the fact that it is horrible. :)

The problem is that enum is not defined as a valid type for Json so the whole concept does give us some problems. One solution is to translate enum values into String with the name of the enum first, and then the name of value like MyFirstEnum.first1. This representation is what Dart gives you if calling toString() on a enum value.

This is fine but for safety we could also add a magic string in the beginning like DART_ENUM:MyFirstEnum.first1 so it is easier to recognize between other strings which could have the same name as the enum value without being an enum.

Next is type safety. In Json, we know that all maps has String as the type of keys. By making our own representation of enum and allowing it to also be keys, we cannot expect a decoder to return e.g. Map<String, dynamic> but must return Map<dynamic, dynamic>.

With that said, here is my attempt to build a Json decoder and encoder which handles enum values. It also works for enum keys in maps:

import 'dart:convert';

class JsonConverterWithEnumSupport {
  final String magicString;
  final Set<Object> allEnumValues = {};
  final Map<String, Object> enumStringToEnumValue = {};

  JsonConverterWithEnumSupport(List<List<Object>>? enumsValues,
      {this.magicString = "DART_ENUM:"}) {
    enumsValues?.forEach(addEnumValues);
  }

  void addEnumValues(List<Object> enumValues) {
    for (final enumValue in enumValues) {
      enumStringToEnumValue[enumValue.toString()] = enumValue;
      allEnumValues.add(enumValue);
    }
  }

  String _addMagic(dynamic enumValue) => '$magicString$enumValue';
  String _removeMagic(String string) => string.substring(magicString.length);

  String encode(Object? value) =>
      json.encode(value, toEncodable: (dynamic object) {
        if (object is Map) {
          return object.map<dynamic, dynamic>((dynamic key, dynamic value) =>
              MapEntry<dynamic, dynamic>(
                  allEnumValues.contains(key) ? _addMagic(key) : key,
                  allEnumValues.contains(value) ? _addMagic(value) : value));
        }

        if (object is List) {
          return object.map<dynamic>(
              (dynamic e) => allEnumValues.contains(e) ? _addMagic(e) : e);
        }

        if (allEnumValues.contains(object)) {
          return _addMagic(object);
        }

        return object;
      });

  dynamic decode(String source) => json.decode(source, reviver: (key, value) {
        if (value is String && value.startsWith(magicString)) {
          return enumStringToEnumValue[_removeMagic(value)];
        }

        if (value is Map) {
          return value.map<dynamic, dynamic>((dynamic key, dynamic value) =>
              MapEntry<dynamic, dynamic>(
                  (key is String) && key.startsWith(magicString)
                      ? enumStringToEnumValue[_removeMagic(key)]
                      : key,
                  value));
        }

        return value;
      });
}

enum MyFirstEnum { first1, first2 }
enum MySecondEnum { second1, second2 }

void main() {
  final converter =
      JsonConverterWithEnumSupport([MyFirstEnum.values, MySecondEnum.values]);

  final jsonString = converter.encode({
    MyFirstEnum.first1: [MySecondEnum.second2, MySecondEnum.second1],
    'test': {MyFirstEnum.first2: 5}
  });

  print(jsonString);
  // {"DART_ENUM:MyFirstEnum.first1":["DART_ENUM:MySecondEnum.second2","DART_ENUM:MySecondEnum.second1"],"test":{"DART_ENUM:MyFirstEnum.first2":5}}
  print(converter.decode(jsonString));
  // {MyFirstEnum.first1: [MySecondEnum.second2, MySecondEnum.second1], test: {MyFirstEnum.first2: 5}}
}

You will need to feed into JsonConverterWithEnumSupport all the possible enum values there is possible (see the main method in the bottom for example).

If you don't want the magic string to be appended on each enum you can just create JsonConverterWithEnumSupport with: magicString: '' as parameter.

Upvotes: 1

Thetyne
Thetyne

Reputation: 225

you can use describeEnum method inside foundation.dart

Upvotes: 1

Guillaume Roux
Guillaume Roux

Reputation: 7328

You could create an extension on your enum to convert it to a String then convert your map to a Map<String, int> so it will be encoded correctly:

import 'dart:convert';

enum MyEnum { type }

extension MyEnumModifier on MyEnum {
  String get string => this.toString().split('.').last;
}

void main() {
  Map<MyEnum, int> map = {MyEnum.type: 10};
  Map<String, int> newMap = {};
  
  map.forEach((key, value) =>
    newMap[key.string] = value);
  
  final json = jsonEncode(newMap);
  print(json);
}

Output

{"type":10}

Upvotes: 1

Related Questions