Search code examples
jsondartfreezedjson-serializable

How to convert a Map to a List of keys


Suppose I have the following model (using freezed):

@freezed
class User with _$User {
  const factory User({
    required UserID id,
    required List<String> categories,
  }) = _User;

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

I'd like to deserialize this model from a JSON object that looks like this:

{
  "id": "1234",
  "categories": {
    "A": true,
    "B": true
  }
}

Obviously the default serialization does not work, so I tried implementing a custom JsonConverter:

class ListMapConverter<K, V> implements JsonConverter<List<K>, Map<K, V>> {
  const ListMapConverter(this._value);

  final V _value;

  @override
  List<K> fromJson(Map<K, V> json) => json.keys.toList();

  @override
  Map<K, V> toJson(List<K> object) => {for (final e in object) e: _value};
}

I annotated the model's property like so:

@ListMapConverter<String, bool>(true) required List<String> categories

This does not work, and the custom converter is not used in the generated output:

_$UserImpl _$$UserImplFromJson(Map<String, dynamic> json) => _$UserImpl(
      id: UserID.fromJson(json['id'] as String),
      categories: (json['categories'] as List<dynamic>)
          .map((e) => e as String)
          .toList(),
    );

Map<String, dynamic> _$$UserImplToJson(_$UserImpl instance) =>
    <String, dynamic>{
      'id': instance.id,
      'categories': instance.categories,
    };

How can I fix this? Is there a better way to convert Maps to Lists and vice versa using freezed/json_serializable? Thanks.


Solution

  • The problem seems to be json_serializable silently ignoring your generic converter. On top of that, json_serializable does not support converters with constructor arguments, see this issue.

    A workaround would be to make your converter non-generic and use named constructors to provide the instance variable _value:

    class ListMapConverter implements JsonConverter<List<String>, Map<String, bool>> {
      // const ListMapConverter(this._value);
      const ListMapConverter.zero() : _value = false;
      const ListMapConverter.one() : _value = true;
    
      final bool _value;
    
      @override
      List<String> fromJson(Map<String, bool> json) => json.keys.toList();
    
      @override
      Map<String, bool> toJson(List<String> object) => {for (final e in object) e: _value};
    }
    

    You could then use the annotation: @ListMapConverter.one().

    In view of all this, it might be easier to extend the class User manually by adding the method toJson and the factory constructor fromJson.

    You could still use freezed to generate other convenience methods, like copyWith etc.