Skip to content

feat: implement first version of sealed class serialization #1483

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions example/lib/complex_sealed_class_examples.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import 'package:json_annotation/json_annotation.dart';

part 'complex_sealed_class_examples.g.dart';

@JsonSerializable(
unionDiscriminator: 'organization',
)
sealed class Organization {
final String name;

Organization({required this.name});

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

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

@JsonSerializable(
unionDiscriminator: 'department',
)
sealed class Department extends Organization {
final String departmentHead;

Department({
required this.departmentHead,
required super.name,
});

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

@JsonSerializable()
class Team extends Department {
final String teamLead;

Team({
required this.teamLead,
required super.departmentHead,
required super.name,
});
}
52 changes: 52 additions & 0 deletions example/lib/complex_sealed_class_examples.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 38 additions & 0 deletions example/lib/sealed_class_example.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import 'package:json_annotation/json_annotation.dart';

part 'sealed_class_example.g.dart';

@JsonSerializable(
unionDiscriminator: 'vehicle_type',
unionRename: UnionRename.snake,
)
sealed class Vehicle {
final String vehicleID;

Vehicle({required this.vehicleID});

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

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

@JsonSerializable()
class Car extends Vehicle {
final int numberOfDoors;

Car({
required this.numberOfDoors,
required super.vehicleID,
});
}

@JsonSerializable()
class Bicycle extends Vehicle {
final bool hasBell;

Bicycle({
required this.hasBell,
required super.vehicleID,
});
}
46 changes: 46 additions & 0 deletions example/lib/sealed_class_example.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ environment:
resolution: workspace

dependencies:
json_annotation: ^4.9.0
json_annotation: ^4.10.0-wip

dev_dependencies:
# Used by tests. Not required to use `json_serializable`.
Expand All @@ -21,7 +21,7 @@ dev_dependencies:
build_verify: ^3.0.0

# REQUIRED!
json_serializable: ^6.8.0
json_serializable: ^6.10.0

# Not required to use `json_serializable`.
path: ^1.8.0
Expand Down
4 changes: 3 additions & 1 deletion json_annotation/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## 4.9.1-wip
## 4.10.0-wip

- Add `JsonSerializable.unionRename`
- Add `JsonSerializable.unionDiscriminator`
- Require Dart 3.8

## 4.9.0
Expand Down
17 changes: 17 additions & 0 deletions json_annotation/lib/src/allowed_keys_helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,23 @@ class UnrecognizedKeysException extends BadKeyException {
: super._(map);
}

/// Exception thrown if there is an unrecognized union type in a JSON map
/// that was provided during deserialization.
class UnrecognizedUnionTypeException extends BadKeyException {
/// The discriminator that was not recognized.
final String unrecognizedType;

/// The type of the union that was being deserialized.
final Type unionType;

@override
String get message => 'Unrecognized type: $unrecognizedType '
'for union: $unionType.';

UnrecognizedUnionTypeException(this.unrecognizedType, this.unionType, Map map)
: super._(map);
}

/// Exception thrown if there are missing required keys in a JSON map that was
/// provided during deserialization.
class MissingRequiredKeysException extends BadKeyException {
Expand Down
40 changes: 40 additions & 0 deletions json_annotation/lib/src/json_serializable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,26 @@ enum FieldRename {
screamingSnake,
}

/// Values for the automatic class renaming behavior for [JsonSerializable]
/// with sealed classes.
enum UnionRename {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sad that we're creating a new type here. We could just use FieldRename?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thought process was that it might be confusing for the package users if union rename is of type FieldRename, since its renaming class names and not any field names. Although after the renaming the string is used as key of a map so there the field would make a bit more sense again.

Technically there shouldn't be a limitation for not just using FieldRename since they have the same rename options (at least now, probably no need to ever have different options). So should we remove the UnionRename?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our users will be fine. A better thing would probably be to rename FieldRename to just be RenameType and then typedef FieldRename = RenameType; and mark as deprecated.

Just put a todo in for that and I can follow-up.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, this is now done. Super nice and handy the typedef deprecation trick!

/// Use the union class name without changes.
none,

/// Encodes union class named `KebabCase` with a JSON key `kebab-case`.
kebab,

/// Encodes union class named `SnakeCase` with a JSON key `snake_case`.
snake,

/// Encodes union class named `PascalCase` with a JSON key `PascalCase`.
pascal,

/// Encodes union class named `ScreamingSnakeCase` with a JSON key
/// `SCREAMING_SNAKE_CASE`
screamingSnake,
}

/// An annotation used to specify a class to generate code for.
@JsonSerializable(
checked: true,
Expand Down Expand Up @@ -224,6 +244,20 @@ class JsonSerializable {
/// `includeIfNull`, that value takes precedent.
final bool? includeIfNull;

/// The discriminator key used to identify the union type.
///
/// Defaults to `type`.
final String? unionDiscriminator;

/// Defines the automatic naming strategy when converting class names
/// to union type names.
///
/// With a value [UnionRename.none] (the default), the name of the class is
/// used without modification.
///
/// See [UnionRename] for details on the other options.
final UnionRename? unionRename;

/// A list of [JsonConverter] to apply to this class.
///
/// Writing:
Expand Down Expand Up @@ -276,6 +310,8 @@ class JsonSerializable {
this.converters,
this.genericArgumentFactories,
this.createPerFieldToJson,
this.unionDiscriminator,
this.unionRename,
});

factory JsonSerializable.fromJson(Map<String, dynamic> json) =>
Expand All @@ -296,6 +332,8 @@ class JsonSerializable {
ignoreUnannotated: false,
includeIfNull: true,
genericArgumentFactories: false,
unionDiscriminator: 'type',
unionRename: UnionRename.none,
);

/// Returns a new [JsonSerializable] instance with fields equal to the
Expand All @@ -318,6 +356,8 @@ class JsonSerializable {
includeIfNull: includeIfNull ?? defaults.includeIfNull,
genericArgumentFactories:
genericArgumentFactories ?? defaults.genericArgumentFactories,
unionDiscriminator: unionDiscriminator ?? defaults.unionDiscriminator,
unionRename: unionRename ?? defaults.unionRename,
);

Map<String, dynamic> toJson() => _$JsonSerializableToJson(this);
Expand Down
22 changes: 22 additions & 0 deletions json_annotation/lib/src/json_serializable.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion json_annotation/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: json_annotation
version: 4.9.1-wip
version: 4.10.0-wip
description: >-
Classes and helper functions that support JSON code generation via the
`json_serializable` package.
Expand Down
4 changes: 3 additions & 1 deletion json_serializable/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## 6.9.6-wip
## 6.10.0

- Add support for deserializing union json to sealed class
- Add support for serializing sealed class to union json
- Move `package:collection` to a dev dependency.
- Use new `null-aware element` feature in generated code.
- Require Dart 3.8
Expand Down
Loading
Loading