json_serializable.dart icon indicating copy to clipboard operation
json_serializable.dart copied to clipboard

[Feature request] Support dart3 sealed class

Open lifeapps-42 opened this issue 11 months ago • 6 comments

Thank you for this package!

I failed to find any discussions about this and it feels like essential feture at this moment.

Do you have any plan to implement union-like sealed class serialization, so we can avoid using freezed?

We could use this feature something like this (inspired by freezed):

@JsonSerializable(unionKey: 'subtype') // dafualt 'runtimeType'
sealed class RootSealedClass {
  const RootSealedClass();

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

  @JsonValue('first') // or literal class name as default
  const factory RootSealedClass.first(String someAtribute) =
      FirstSealedClassSubtype;

  @JsonValue('second') // or literal class name as default
  const factory RootSealedClass.second(int someOtherAtribute) =
      SecondSealedClassSubtype;

  Map<String, dynamic> toJson();
}

@JsonSerializable()
class FirstSealedClassSubtype extends RootSealedClass {
  const FirstSealedClassSubtype(this.someAtribute);

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

  final String someAtribute;

  @override
  Map<String, dynamic> toJson() => _$FirstSealedClassSubtypeToJson(this);
}

@JsonSerializable()
class SecondSealedClassSubtype extends RootSealedClass {
  const SecondSealedClassSubtype(this.someOtherAtribute);

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

  final int someOtherAtribute;

  @override
  Map<String, dynamic> toJson() => _$SecondSealedClassSubtypeToJson(this);
}

lifeapps-42 avatar Aug 04 '23 14:08 lifeapps-42

Maybe? How does Freezed do this?

kevmoo avatar Aug 05 '23 01:08 kevmoo

FromJson methods for subtypes are generated as usual:

FirstSealedClassSubtype _$firstSealedClassSubtypeFromJson(Map<String, dynamic> json) =>
    FirstSealedClassSubtype(
      someAtribute: json['someAtribute'] as String,
    );

SecondSealedClassSubtype _$secondSealedClassSubtypeFromJson(Map<String, dynamic> json) =>
    SecondSealedClassSubtype(
      someOtherAtribute: json['someOtherAtribute'] as int,
    );

ToJson methods just need one additional key member called 'subtype' in our case:

Map<String, dynamic> _$firstSealedClassSubtypeToJson(FirstSealedClassSubtype instance) =>
    <String, dynamic>{
      'someAtribute': instance.someAtribute,
      'subtype': 'first', // the value is specified by user inside @JsonValue('first') (see my first comment)
    };

Map<String, dynamic> _$secondSealedClassSubtypeToJson(SecondSealedClassSubtype instance) =>
    <String, dynamic>{
      'someOtherAtribute': instance.someOtherAtribute,
      'subtype': 'second',
    };

Plus we do need root fromJson implementation. That could be just redirecting map/switch:

RootSealedClass _$rootSealedClassFromJson(Map<String, dynamic> json) =>
    switch (json['subtype']) {
      'first' => _$firstSealedClassSubtypeFromJson(json),
      'second' => _$secondSealedClassSubtypeFromJson(json),
      _ => throw ArgumentError() // or some fallback?
    };

The root toJson method should be abstract (so I editted my first comment)

lifeapps-42 avatar Aug 06 '23 15:08 lifeapps-42

I'd take a PR here – It's hard to judge if the complexity is worth it without seeing the PR – which makes me nervous about saying you should make the PR, though. 😄

kevmoo avatar Aug 08 '23 22:08 kevmoo

I'd take a PR here – It's hard to judge if the complexity is worth it without seeing the PR – which makes me nervous about saying you should make the PR, though. 😄

I have precisely 0 exp with code generation)) But I'll try

lifeapps-42 avatar Aug 11 '23 12:08 lifeapps-42

This would be a nice addition.

Currently, we can implement it by manually marking the sealed class subtypes with @JsonSerializable, but if we want to have a fromJson in the sealed class we have to implement it manually, which is a little burdensome and error-prone...

mateusfccp avatar Aug 28 '23 17:08 mateusfccp