google / json_serializable.dart

Generates utilities to aid in serializing to/from JSON.
https://pub.dev/packages/json_serializable
BSD 3-Clause "New" or "Revised" License
1.55k stars 395 forks source link

Enum w/ custom toJson as a Map key...is broken #1303

Closed fzyzcjy closed 1 year ago

fzyzcjy commented 1 year ago

Minimal reproducible sample

import 'dart:convert';

import 'package:json_annotation/json_annotation.dart';

part 'main.g.dart';

enum MyEnum {
  a, b;
  String toJson() => name; // or whatever custom implementation
}

@JsonSerializable()
class Apple {
  final Map<MyEnum, int> myMap;

  const Apple(this.myMap);

  factory Apple.fromJson(Map<String, dynamic> json) => _$AppleFromJson(json);

  Map<String, dynamic> toJson() => _$AppleToJson(this);
}

void main() {
  final apple = Apple({MyEnum.a: 42});
  try {
    jsonEncode(apple);
  }on JsonUnsupportedObjectError catch(e) {
    print('cause=${e.cause} partialResult=${e.partialResult}');
    rethrow;
  }
}

// auto generated code

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'main.dart';

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

Apple _$AppleFromJson(Map<String, dynamic> json) => Apple(
      (json['myMap'] as Map<String, dynamic>).map(
        (k, e) => MapEntry($enumDecode(_$MyEnumEnumMap, k), e as int),
      ),
    );

Map<String, dynamic> _$AppleToJson(Apple instance) => <String, dynamic>{
      'myMap': instance.myMap,
    };

const _$MyEnumEnumMap = {
  MyEnum.a: 'a',
  MyEnum.b: 'b',
};

Run it:

dart --enable-asserts main.dart
cause=Converting object to an encodable object failed: _Map len:1 partialResult={"myMap":
Unhandled exception:
Converting object to an encodable object failed: Instance of 'Apple'
#0      _JsonStringifier.writeObject (dart:convert/json.dart:793:7)
#1      _JsonStringStringifier.printOn (dart:convert/json.dart:982:17)
#2      _JsonStringStringifier.stringify (dart:convert/json.dart:967:5)
#3      JsonEncoder.convert (dart:convert/json.dart:345:30)
#4      JsonCodec.encode (dart:convert/json.dart:231:45)
#5      jsonEncode (dart:convert/json.dart:114:10)
#6      main (package:bug/main.dart:26:5)
#7      _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:297:19)

Alternatives: Adding explicitToJson:true, or making the key enum has no toJson does work

import 'dart:convert';

import 'package:json_annotation/json_annotation.dart';

part 'main.g.dart';

enum MyEnum {
  a, b;
  String toJson() => name; // or whatever custom implementation
}

enum MyEnum2 { a, b }

@JsonSerializable(explicitToJson: true)
class Apple {
  final Map<MyEnum, int> myMap;
  final Map<MyEnum2, int> myMap2;

  const Apple(this.myMap, this.myMap2);

  factory Apple.fromJson(Map<String, dynamic> json) => _$AppleFromJson(json);

  Map<String, dynamic> toJson() => _$AppleToJson(this);
}

void main() {
  final apple = Apple({MyEnum.a: 42}, {MyEnum2.a: 42});
  try {
    jsonEncode(apple);
  }on JsonUnsupportedObjectError catch(e) {
    print('cause=${e.cause} partialResult=${e.partialResult}');
    rethrow;
  }
}

// generated code

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'main.dart';

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

Apple _$AppleFromJson(Map<String, dynamic> json) => Apple(
      (json['myMap'] as Map<String, dynamic>).map(
        (k, e) => MapEntry($enumDecode(_$MyEnumEnumMap, k), e as int),
      ),
      (json['myMap2'] as Map<String, dynamic>).map(
        (k, e) => MapEntry($enumDecode(_$MyEnum2EnumMap, k), e as int),
      ),
    );

Map<String, dynamic> _$AppleToJson(Apple instance) => <String, dynamic>{
      'myMap': instance.myMap.map((k, e) => MapEntry(k.toJson(), e)),
      'myMap2':
          instance.myMap2.map((k, e) => MapEntry(_$MyEnum2EnumMap[k]!, e)),
    };

const _$MyEnumEnumMap = {
  MyEnum.a: 'a',
  MyEnum.b: 'b',
};

const _$MyEnum2EnumMap = {
  MyEnum2.a: 'a',
  MyEnum2.b: 'b',
};
kevmoo commented 1 year ago

Good catch!

kevmoo commented 1 year ago

So. It turns out this works IF you add a corresponding fromJson factory ctor to your enum. Then all is good!

Going to close this out, but digging in here did help me find this fix – https://github.com/google/json_serializable.dart/pull/1307 – so thanks!