diff --git a/_test_yaml/test/src/build_config.dart b/_test_yaml/test/src/build_config.dart index 958329f8d..9a7d6e16b 100644 --- a/_test_yaml/test/src/build_config.dart +++ b/_test_yaml/test/src/build_config.dart @@ -85,7 +85,7 @@ class Builder { Map toJson() => _$BuilderToJson(this); } -@JsonEnum(fieldRename: FieldRename.snake) +@JsonEnum(fieldRename: RenameType.snake) enum AutoApply { none, dependents, allPackages, rootPackage } enum BuildTo { cache, source } diff --git a/example/lib/complex_sealed_class_examples.dart b/example/lib/complex_sealed_class_examples.dart new file mode 100644 index 000000000..e254594f6 --- /dev/null +++ b/example/lib/complex_sealed_class_examples.dart @@ -0,0 +1,36 @@ +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 json) => + _$OrganizationFromJson(json); + + Map 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 json) => + _$DepartmentFromJson(json); +} + +@JsonSerializable() +class Team extends Department { + final String teamLead; + + Team({ + required this.teamLead, + required super.departmentHead, + required super.name, + }); +} diff --git a/example/lib/complex_sealed_class_examples.g.dart b/example/lib/complex_sealed_class_examples.g.dart new file mode 100644 index 000000000..8a8d0f06c --- /dev/null +++ b/example/lib/complex_sealed_class_examples.g.dart @@ -0,0 +1,52 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'complex_sealed_class_examples.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Organization _$OrganizationFromJson(Map json) => + switch (json['organization']) { + 'Department' => _$DepartmentFromJson(json), + _ => throw UnrecognizedUnionTypeException( + '${json['organization']}', + Organization, + json, + ), + }; + +Map _$OrganizationToJson(Organization instance) => + switch (instance) { + final Department instance => { + 'organization': 'Department', + ..._$DepartmentToJson(instance), + }, + }; + +Department _$DepartmentFromJson(Map json) => + switch (json['department']) { + 'Team' => _$TeamFromJson(json), + _ => throw UnrecognizedUnionTypeException( + '${json['department']}', + Department, + json, + ), + }; + +Map _$DepartmentToJson(Department instance) => + switch (instance) { + final Team instance => {'department': 'Team', ..._$TeamToJson(instance)}, + }; + +Team _$TeamFromJson(Map json) => Team( + teamLead: json['teamLead'] as String, + departmentHead: json['departmentHead'] as String, + name: json['name'] as String, +); + +Map _$TeamToJson(Team instance) => { + 'name': instance.name, + 'departmentHead': instance.departmentHead, + 'teamLead': instance.teamLead, +}; diff --git a/example/lib/sealed_class_example.dart b/example/lib/sealed_class_example.dart new file mode 100644 index 000000000..328baca0e --- /dev/null +++ b/example/lib/sealed_class_example.dart @@ -0,0 +1,32 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'sealed_class_example.g.dart'; + +@JsonSerializable( + unionDiscriminator: 'vehicle_type', + unionRename: RenameType.snake, +) +sealed class Vehicle { + final String vehicleID; + + Vehicle({required this.vehicleID}); + + factory Vehicle.fromJson(Map json) => + _$VehicleFromJson(json); + + Map 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}); +} diff --git a/example/lib/sealed_class_example.g.dart b/example/lib/sealed_class_example.g.dart new file mode 100644 index 000000000..20b4bfd46 --- /dev/null +++ b/example/lib/sealed_class_example.g.dart @@ -0,0 +1,46 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'sealed_class_example.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Vehicle _$VehicleFromJson(Map json) => + switch (json['vehicle_type']) { + 'car' => _$CarFromJson(json), + 'bicycle' => _$BicycleFromJson(json), + _ => throw UnrecognizedUnionTypeException( + '${json['vehicle_type']}', + Vehicle, + json, + ), + }; + +Map _$VehicleToJson(Vehicle instance) => switch (instance) { + final Car instance => {'vehicle_type': 'car', ..._$CarToJson(instance)}, + final Bicycle instance => { + 'vehicle_type': 'bicycle', + ..._$BicycleToJson(instance), + }, +}; + +Car _$CarFromJson(Map json) => Car( + numberOfDoors: (json['numberOfDoors'] as num).toInt(), + vehicleID: json['vehicleID'] as String, +); + +Map _$CarToJson(Car instance) => { + 'vehicleID': instance.vehicleID, + 'numberOfDoors': instance.numberOfDoors, +}; + +Bicycle _$BicycleFromJson(Map json) => Bicycle( + hasBell: json['hasBell'] as bool, + vehicleID: json['vehicleID'] as String, +); + +Map _$BicycleToJson(Bicycle instance) => { + 'vehicleID': instance.vehicleID, + 'hasBell': instance.hasBell, +}; diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 392a1cd2f..9244fae6a 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -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`. @@ -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 diff --git a/json_annotation/CHANGELOG.md b/json_annotation/CHANGELOG.md index ae0b4d43d..0473d8af2 100644 --- a/json_annotation/CHANGELOG.md +++ b/json_annotation/CHANGELOG.md @@ -1,6 +1,9 @@ -## 4.9.1-wip +## 4.10.0-wip +- Add `JsonSerializable.unionRename` +- Add `JsonSerializable.unionDiscriminator` - Require Dart 3.8 +- Deprecated `FieldRename` in favor of `RenameType` ## 4.9.0 diff --git a/json_annotation/lib/src/allowed_keys_helpers.dart b/json_annotation/lib/src/allowed_keys_helpers.dart index 5ea896a79..6729ef330 100644 --- a/json_annotation/lib/src/allowed_keys_helpers.dart +++ b/json_annotation/lib/src/allowed_keys_helpers.dart @@ -79,6 +79,24 @@ 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 { diff --git a/json_annotation/lib/src/json_enum.dart b/json_annotation/lib/src/json_enum.dart index 27d1fb5bc..621409873 100644 --- a/json_annotation/lib/src/json_enum.dart +++ b/json_annotation/lib/src/json_enum.dart @@ -12,7 +12,7 @@ import 'json_value.dart'; class JsonEnum { const JsonEnum({ this.alwaysCreate = false, - this.fieldRename = FieldRename.none, + this.fieldRename = RenameType.none, this.valueField, }); @@ -27,14 +27,14 @@ class JsonEnum { /// Defines the naming strategy when converting enum entry names to JSON /// values. /// - /// With a value [FieldRename.none] (the default), the name of the enum entry + /// With a value [RenameType.none] (the default), the name of the enum entry /// is used without modification. /// - /// See [FieldRename] for details on the other options. + /// See [RenameType] for details on the other options. /// /// Note: the value for [JsonValue.value] takes precedence over this option /// for entries annotated with [JsonValue]. - final FieldRename fieldRename; + final RenameType fieldRename; /// Specifies the field within an "enhanced enum" to use as the value /// to use for serialization. diff --git a/json_annotation/lib/src/json_serializable.dart b/json_annotation/lib/src/json_serializable.dart index 7c82e2f2e..9349ba671 100644 --- a/json_annotation/lib/src/json_serializable.dart +++ b/json_annotation/lib/src/json_serializable.dart @@ -12,8 +12,12 @@ import 'json_key.dart'; part 'json_serializable.g.dart'; +// TODO: Remove typedef +@Deprecated('Use RenameType instead') +typedef FieldRename = RenameType; + /// Values for the automatic field renaming behavior for [JsonSerializable]. -enum FieldRename { +enum RenameType { /// Use the field name without changes. none, @@ -35,7 +39,7 @@ enum FieldRename { @JsonSerializable( checked: true, disallowUnrecognizedKeys: true, - fieldRename: FieldRename.snake, + fieldRename: RenameType.snake, ) @Target({TargetKind.classType}) class JsonSerializable { @@ -153,14 +157,14 @@ class JsonSerializable { /// Defines the automatic naming strategy when converting class field names /// into JSON map keys. /// - /// With a value [FieldRename.none] (the default), the name of the field is + /// With a value [RenameType.none] (the default), the name of the field is /// used without modification. /// - /// See [FieldRename] for details on the other options. + /// See [RenameType] for details on the other options. /// /// Note: the value for [JsonKey.name] takes precedence over this option for /// fields annotated with [JsonKey]. - final FieldRename? fieldRename; + final RenameType? fieldRename; /// When `true` on classes with type parameters (generic types), extra /// "helper" parameters will be generated for `fromJson` and/or `toJson` to @@ -224,6 +228,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 [RenameType.none] (the default), the name of the class is + /// used without modification. + /// + /// See [RenameType] for details on the other options. + final RenameType? unionRename; + /// A list of [JsonConverter] to apply to this class. /// /// Writing: @@ -276,6 +294,8 @@ class JsonSerializable { this.converters, this.genericArgumentFactories, this.createPerFieldToJson, + this.unionDiscriminator, + this.unionRename, }); factory JsonSerializable.fromJson(Map json) => @@ -292,10 +312,12 @@ class JsonSerializable { createToJson: true, disallowUnrecognizedKeys: false, explicitToJson: false, - fieldRename: FieldRename.none, + fieldRename: RenameType.none, ignoreUnannotated: false, includeIfNull: true, genericArgumentFactories: false, + unionDiscriminator: 'type', + unionRename: RenameType.none, ); /// Returns a new [JsonSerializable] instance with fields equal to the @@ -318,6 +340,8 @@ class JsonSerializable { includeIfNull: includeIfNull ?? defaults.includeIfNull, genericArgumentFactories: genericArgumentFactories ?? defaults.genericArgumentFactories, + unionDiscriminator: unionDiscriminator ?? defaults.unionDiscriminator, + unionRename: unionRename ?? defaults.unionRename, ); Map toJson() => _$JsonSerializableToJson(this); diff --git a/json_annotation/lib/src/json_serializable.g.dart b/json_annotation/lib/src/json_serializable.g.dart index 80a3e4c86..794812ce5 100644 --- a/json_annotation/lib/src/json_serializable.g.dart +++ b/json_annotation/lib/src/json_serializable.g.dart @@ -29,6 +29,8 @@ JsonSerializable _$JsonSerializableFromJson( 'generic_argument_factories', 'ignore_unannotated', 'include_if_null', + 'union_discriminator', + 'union_rename', ], ); final val = JsonSerializable( @@ -46,7 +48,7 @@ JsonSerializable _$JsonSerializableFromJson( explicitToJson: $checkedConvert('explicit_to_json', (v) => v as bool?), fieldRename: $checkedConvert( 'field_rename', - (v) => $enumDecodeNullable(_$FieldRenameEnumMap, v), + (v) => $enumDecodeNullable(_$RenameTypeEnumMap, v), ), ignoreUnannotated: $checkedConvert( 'ignore_unannotated', @@ -61,6 +63,14 @@ JsonSerializable _$JsonSerializableFromJson( 'create_per_field_to_json', (v) => v as bool?, ), + unionDiscriminator: $checkedConvert( + 'union_discriminator', + (v) => v as String?, + ), + unionRename: $checkedConvert( + 'union_rename', + (v) => $enumDecodeNullable(_$RenameTypeEnumMap, v), + ), ); return val; }, @@ -77,6 +87,8 @@ JsonSerializable _$JsonSerializableFromJson( 'includeIfNull': 'include_if_null', 'genericArgumentFactories': 'generic_argument_factories', 'createPerFieldToJson': 'create_per_field_to_json', + 'unionDiscriminator': 'union_discriminator', + 'unionRename': 'union_rename', }, ); @@ -92,16 +104,18 @@ Map _$JsonSerializableToJson(JsonSerializable instance) => 'create_to_json': instance.createToJson, 'disallow_unrecognized_keys': instance.disallowUnrecognizedKeys, 'explicit_to_json': instance.explicitToJson, - 'field_rename': _$FieldRenameEnumMap[instance.fieldRename], + 'field_rename': _$RenameTypeEnumMap[instance.fieldRename], 'generic_argument_factories': instance.genericArgumentFactories, 'ignore_unannotated': instance.ignoreUnannotated, 'include_if_null': instance.includeIfNull, + 'union_discriminator': instance.unionDiscriminator, + 'union_rename': _$RenameTypeEnumMap[instance.unionRename], }; -const _$FieldRenameEnumMap = { - FieldRename.none: 'none', - FieldRename.kebab: 'kebab', - FieldRename.snake: 'snake', - FieldRename.pascal: 'pascal', - FieldRename.screamingSnake: 'screamingSnake', +const _$RenameTypeEnumMap = { + RenameType.none: 'none', + RenameType.kebab: 'kebab', + RenameType.snake: 'snake', + RenameType.pascal: 'pascal', + RenameType.screamingSnake: 'screamingSnake', }; diff --git a/json_annotation/pubspec.yaml b/json_annotation/pubspec.yaml index f1cd2c79b..c2fe78425 100644 --- a/json_annotation/pubspec.yaml +++ b/json_annotation/pubspec.yaml @@ -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. diff --git a/json_serializable/CHANGELOG.md b/json_serializable/CHANGELOG.md index f8c38271e..884c786a3 100644 --- a/json_serializable/CHANGELOG.md +++ b/json_serializable/CHANGELOG.md @@ -1,7 +1,10 @@ +## 6.11.0-wip + +- Add support for deserializing union json to sealed class +- Add support for serializing sealed class to union json + ## 6.10.0 -- Required `analyzer: ^7.4.0`. -- Switch to analyzer element2 model and `build: ^3.0.0-dev`. - Move `package:collection` to a dev dependency. - Use new `null-aware element` feature in generated code. - Require Dart 3.8 diff --git a/json_serializable/README.md b/json_serializable/README.md index 47c0d0cbb..b5c583f0b 100644 --- a/json_serializable/README.md +++ b/json_serializable/README.md @@ -104,7 +104,7 @@ precedence over any value set on [`JsonSerializable`]. Annotate `enum` types with [`JsonEnum`] (new in `json_annotation` 4.2.0) to: 1. Specify the default rename logic for each enum value using `fieldRename`. For - instance, use `fieldRename: FieldRename.kebab` to encode `enum` value + instance, use `fieldRename: RenameType.kebab` to encode `enum` value `noGood` as `"no-good"`. 1. Force the generation of the `enum` helpers, even if the `enum` is not referenced in code. This is an edge scenario, but useful for some. @@ -253,6 +253,53 @@ customize the encoding/decoding of any type, you have a few options. } ``` +# Sealed classes + +As of `json_serializable` version 6.10.0 and `json_annotation` +version 4.10.0, sealed classes can be serialized to json unions and json unions +can be deserialized to sealed classes. + +To achieve this, both the sealed class and its subclasses should be annotated +with [`JsonSerializable`]. Only the sealed class should have `fromJson` factory +or `toJson` function. To customize the sealed class behavior, use the fields +`unionRename` and `unionDiscriminator` in [`JsonSerializable`] or adjust the +default behavior by changing the corresponding fields in `build.yaml`. For +more complex examples, please see [example]: + +```dart +import 'package:json_annotation/json_annotation.dart'; + +part 'sealed_example.g.dart'; + +@JsonSerializable() +sealed class SealedBase { + const SealedBase(); + + factory SealedBase.fromJson(Map json) => + _$SealedBaseFromJson(json); + + Map toJson() => _$SealedBaseToJson(this); +} + +@JsonSerializable() +class SealedSub1 extends SealedBase { + final String exampleField1; + + SealedSub1({ + required this.exampleField1, + }); +} + +@JsonSerializable() +class SealedSub2 extends SealedBase { + final String exampleField2; + + SealedSub2({ + required this.exampleField2, + }); +} +``` + # Build configuration Aside from setting arguments on the associated annotation classes, you can also @@ -282,6 +329,8 @@ targets: generic_argument_factories: false ignore_unannotated: false include_if_null: true + union_discriminator: type + union_rename: none ``` To exclude generated files from coverage, you can further configure `build.yaml`. diff --git a/json_serializable/example/sealed_example.dart b/json_serializable/example/sealed_example.dart new file mode 100644 index 000000000..61a5b44cb --- /dev/null +++ b/json_serializable/example/sealed_example.dart @@ -0,0 +1,27 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'sealed_example.g.dart'; + +@JsonSerializable() +sealed class SealedBase { + const SealedBase(); + + factory SealedBase.fromJson(Map json) => + _$SealedBaseFromJson(json); + + Map toJson() => _$SealedBaseToJson(this); +} + +@JsonSerializable() +class SealedSub1 extends SealedBase { + final String exampleField1; + + SealedSub1({required this.exampleField1}); +} + +@JsonSerializable() +class SealedSub2 extends SealedBase { + final String exampleField2; + + SealedSub2({required this.exampleField2}); +} diff --git a/json_serializable/example/sealed_example.g.dart b/json_serializable/example/sealed_example.g.dart new file mode 100644 index 000000000..8552b672e --- /dev/null +++ b/json_serializable/example/sealed_example.g.dart @@ -0,0 +1,44 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: lines_longer_than_80_chars, text_direction_code_point_in_literal, inference_failure_on_function_invocation, inference_failure_on_collection_literal + +part of 'sealed_example.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SealedBase _$SealedBaseFromJson(Map json) => + switch (json['type']) { + 'SealedSub1' => _$SealedSub1FromJson(json), + 'SealedSub2' => _$SealedSub2FromJson(json), + _ => throw UnrecognizedUnionTypeException( + '${json['type']}', + SealedBase, + json, + ), + }; + +Map _$SealedBaseToJson(SealedBase instance) => + switch (instance) { + final SealedSub1 instance => { + 'type': 'SealedSub1', + ..._$SealedSub1ToJson(instance), + }, + final SealedSub2 instance => { + 'type': 'SealedSub2', + ..._$SealedSub2ToJson(instance), + }, + }; + +SealedSub1 _$SealedSub1FromJson(Map json) => + SealedSub1(exampleField1: json['exampleField1'] as String); + +Map _$SealedSub1ToJson(SealedSub1 instance) => + {'exampleField1': instance.exampleField1}; + +SealedSub2 _$SealedSub2FromJson(Map json) => + SealedSub2(exampleField2: json['exampleField2'] as String); + +Map _$SealedSub2ToJson(SealedSub2 instance) => + {'exampleField2': instance.exampleField2}; diff --git a/json_serializable/lib/src/decode_helper.dart b/json_serializable/lib/src/decode_helper.dart index 2668b4243..1b85d9df2 100644 --- a/json_serializable/lib/src/decode_helper.dart +++ b/json_serializable/lib/src/decode_helper.dart @@ -27,31 +27,6 @@ mixin DecodeHelper implements HelperCore { Map unavailableReasons, ) { assert(config.createFactory); - final buffer = StringBuffer(); - - final mapType = config.anyMap ? 'Map' : 'Map'; - buffer.write( - '$targetClassReference ' - '${prefix}FromJson${genericClassArgumentsImpl(withConstraints: true)}' - '($mapType json', - ); - - if (config.genericArgumentFactories) { - for (var arg in element.typeParameters2) { - final helperName = fromJsonForType( - arg.instantiate(nullabilitySuffix: NullabilitySuffix.none), - ); - - buffer.write(', ${arg.name3} Function(Object? json) $helperName'); - } - if (element.typeParameters2.isNotEmpty) { - buffer.write(','); - } - } - - buffer.write(')'); - - final fromJsonLines = []; String deserializeFun( String paramOrFieldName, { @@ -86,88 +61,245 @@ mixin DecodeHelper implements HelperCore { ), ).toList(); - if (config.checked) { - final classLiteral = escapeDartString(element.name3!); + final functionBodyParts = switch (( + isSealed: element.isSealed, + isChecked: config.checked, + )) { + (isSealed: true, isChecked: _) => [_createSealedFunctionExpressionBody()], + (isSealed: _, isChecked: true) => [ + _createCheckedFunctionExpressionBody(data, checks, accessibleFields), + ], + _ => _createDefaultFunctionBody(data, checks, deserializeFun), + }; + + return CreateFactoryResult( + _createFromJsonFunctionSignature(functionBodyParts), + data.usedCtorParamsAndFields, + ); + } + + /// Creates the function signature around [functionBodyParts] + /// that will be used to deserialize the class. + /// + /// If [functionBodyParts] has only one element, expression body is used. + /// + /// ```dart + /// ExampleClass _$ExampleClassFromJson(Map json) => + /// /* only body part here */ + /// ``` + /// + /// If [functionBodyParts] has more than one element, block body is used. + /// + /// ```dart + /// ExampleClass _$ExampleClassFromJson(Map json) { + /// /* first body parts here */ + /// + /// return /* last body part here */; + /// } + /// ``` + String _createFromJsonFunctionSignature(Iterable functionBodyParts) { + final mapType = config.anyMap ? 'Map' : 'Map'; - final sectionBuffer = StringBuffer() - ..write(''' + final buffer = StringBuffer() + ..write( + '$targetClassReference ' + '${prefix}FromJson${genericClassArgumentsImpl(withConstraints: true)}' + '($mapType json', + ); + + if (config.genericArgumentFactories) { + for (var arg in element.typeParameters2) { + final helperName = fromJsonForType( + arg.instantiate(nullabilitySuffix: NullabilitySuffix.none), + ); + + buffer.write(', ${arg.name3} Function(Object? json) $helperName'); + } + if (element.typeParameters2.isNotEmpty) { + buffer.write(','); + } + } + + buffer.write(')'); + + if (functionBodyParts case [final single]) { + buffer.write('=> $single'); + } else { + buffer + ..writeln('{') + ..writeAll(functionBodyParts.take(functionBodyParts.length - 1)) + ..writeln('return ${functionBodyParts.last}') + ..writeln('}'); + } + + return buffer.toString(); + } + + /// Creates the body of the function that deserializes a sealed class. + /// + /// For example: + /// ```dart + /// switch (json['type']) { + /// 'FirstSubtype' => _$FirstSubtypeFromJson(json), + /// 'SecondSubtype' => _$SecondSubtypeFromJson(json), + /// _ => throw Exception('Unknown type: ${json['type']}'), + /// }; + /// ``` + String _createSealedFunctionExpressionBody() { + assert(element.isSealed); + + final implementations = sealedSubClasses(element); + + final discriminator = config.unionDiscriminator; + + String buildSingleImpl(ClassElement2 impl) { + final unionName = encodedName(config.unionRename, impl.name3!); + + return "'$unionName' => ${classPrefix(impl)}FromJson(json),"; + } + + final sectionBuffer = StringBuffer() + ..write("switch (json['$discriminator']) {") + ..writeAll(implementations.map(buildSingleImpl), '\n') + ..writeln(''' +_ => throw UnrecognizedUnionTypeException( + '\${json['$discriminator']}', + ${element.name3!}, + json, +),''') + ..writeln('};'); + + return sectionBuffer.toString(); + } + + /// Creates the body of the function that deserializes a class with checked + /// mode. + /// + /// For example: + /// ```dart + /// $checkedCreate( + /// 'FirstSubtype', + /// json, + /// ($checkedConvert) { + /// $checkKeys( + /// json, + /// allowedKeys: const ['value', 'someAttribute'], + /// ); + /// final val = FirstSubtype( + /// $checkedConvert('someAttribute', (v) => v as String), + /// $checkedConvert('value', (v) => v as String), + /// ); + /// return val; + /// }, + /// ); + /// ``` + String _createCheckedFunctionExpressionBody( + _ConstructorData data, + List checks, + Map accessibleFields, + ) { + final classLiteral = escapeDartString(element.name3!); + + final sectionBuffer = StringBuffer() + ..write(''' \$checkedCreate( $classLiteral, json, (\$checkedConvert) {\n''') - ..write(checks.join()) - ..write(''' + ..write(checks.join()) + ..write(''' final val = ${data.content};'''); - for (final fieldName in data.fieldsToSet) { - sectionBuffer.writeln(); - final fieldValue = accessibleFields[fieldName]!; - final safeName = safeNameAccess(fieldValue); - sectionBuffer - ..write(''' + for (final fieldName in data.fieldsToSet) { + sectionBuffer.writeln(); + final fieldValue = accessibleFields[fieldName]!; + final safeName = safeNameAccess(fieldValue); + sectionBuffer + ..write(''' \$checkedConvert($safeName, (v) => ''') - ..write('val.$fieldName = ') - ..write(_deserializeForField(fieldValue, checkedProperty: true)); - - final readValueFunc = jsonKeyFor(fieldValue).readValueFunctionName; - if (readValueFunc != null) { - sectionBuffer.writeln(',readValue: $readValueFunc,'); - } + ..write('val.$fieldName = ') + ..write(_deserializeForField(fieldValue, checkedProperty: true)); - sectionBuffer.write(');'); + final readValueFunc = jsonKeyFor(fieldValue).readValueFunctionName; + if (readValueFunc != null) { + sectionBuffer.writeln(',readValue: $readValueFunc,'); } - sectionBuffer.write('''\n return val; - }'''); + sectionBuffer.write(');'); + } - final fieldKeyMap = Map.fromEntries( - data.usedCtorParamsAndFields - .map((k) => MapEntry(k, nameAccess(accessibleFields[k]!))) - .where((me) => me.key != me.value), - ); + sectionBuffer.write('''\n return val; + }'''); - String fieldKeyMapArg; - if (fieldKeyMap.isEmpty) { - fieldKeyMapArg = ''; - } else { - final mapLiteral = jsonMapAsDart(fieldKeyMap); - fieldKeyMapArg = ', fieldKeyMap: const $mapLiteral'; - } + final fieldKeyMap = Map.fromEntries( + data.usedCtorParamsAndFields + .map((k) => MapEntry(k, nameAccess(accessibleFields[k]!))) + .where((me) => me.key != me.value), + ); - sectionBuffer - ..write(fieldKeyMapArg) - ..write(',);'); - fromJsonLines.add(sectionBuffer.toString()); + String fieldKeyMapArg; + if (fieldKeyMap.isEmpty) { + fieldKeyMapArg = ''; } else { - fromJsonLines.addAll(checks); - - final sectionBuffer = StringBuffer() - ..write(''' - ${data.content}'''); - for (final field in data.fieldsToSet) { - sectionBuffer - ..writeln() - ..write(' ..$field = ') - ..write(deserializeFun(field)); - } - sectionBuffer.writeln(';'); - fromJsonLines.add(sectionBuffer.toString()); + final mapLiteral = jsonMapAsDart(fieldKeyMap); + fieldKeyMapArg = ', fieldKeyMap: const $mapLiteral'; } - if (fromJsonLines.length == 1) { - buffer - ..write('=>') - ..write(fromJsonLines.single); - } else { - buffer - ..write('{') - ..writeAll(fromJsonLines.take(fromJsonLines.length - 1)) - ..write('return ') - ..write(fromJsonLines.last) - ..write('}'); + sectionBuffer + ..write(fieldKeyMapArg) + ..write(',);'); + + return sectionBuffer.toString(); + } + + /// Creates the body of the function that deserializes a class. + /// + /// If there are no checks will return a single constructor invocation. + /// + /// ```dart + /// [ + /// ExampleClass( + /// json['exampleField'] as String, + /// ) + /// ] + /// /* OR with fields to set */ + /// [ + /// ExampleClass( + /// json['exampleField'] as String, + /// ) + /// ..field1 = json['field1'] as String + /// ..field2 = json['field2'] as String + /// ] + /// ``` + /// + /// If there are checks, will return the checks followed by the + /// constructor invocation. + /// ```dart + /// [ + /// $checkKeys( + /// json, + /// allowedKeys: const ['exampleField', 'field1', 'field2'], + /// ), + /// ExampleClass( + /// json['exampleField'] as String, + /// ) + /// ] + /// ``` + List _createDefaultFunctionBody( + _ConstructorData data, + List checks, + String Function(String paramOrFieldName, {FormalParameterElement ctorParam}) + deserializeForField, + ) { + final sectionBuffer = StringBuffer() + ..write(''' + ${data.content}'''); + for (final field in data.fieldsToSet) { + sectionBuffer.writeln('..$field = ${deserializeForField(field)}'); } + sectionBuffer.writeln(';'); - return CreateFactoryResult(buffer.toString(), data.usedCtorParamsAndFields); + return [...checks, sectionBuffer.toString()]; } Iterable _checkKeys(Iterable accessibleFields) sync* { diff --git a/json_serializable/lib/src/encoder_helper.dart b/json_serializable/lib/src/encoder_helper.dart index 6536cddda..2de621912 100644 --- a/json_serializable/lib/src/encoder_helper.dart +++ b/json_serializable/lib/src/encoder_helper.dart @@ -11,6 +11,7 @@ import 'helper_core.dart'; import 'type_helpers/generic_factory_helper.dart'; import 'type_helpers/json_converter_helper.dart'; import 'unsupported_type_error.dart'; +import 'utils.dart'; mixin EncodeHelper implements HelperCore { String _fieldAccess(FieldElement2 field) => @@ -73,7 +74,6 @@ mixin EncodeHelper implements HelperCore { final buffer = StringBuffer( 'abstract final class _\$${element.name3!.nonPrivate}JsonKeys {', ); - // ..write('static const _\$${element.name.nonPrivate}JsonKeys();'); for (final field in accessibleFieldSet) { buffer.writeln( @@ -90,6 +90,23 @@ mixin EncodeHelper implements HelperCore { Iterable createToJson(Set accessibleFields) sync* { assert(config.createToJson); + final expressionBody = element.isSealed + ? _createSealedFunctionExpressionBody() + : _createFieldMapFunctionExpressionBody(accessibleFields); + + yield _createToJsonFunctionSignature(expressionBody); + } + + /// Creates the function signature around [functionExpressionBody] + /// that will be used to serialize the class. + /// + /// For example: + /// + /// ```dart + /// Map _$ExampleClassToJson(ExampleClass instance) => + /// /* expression body here */; + /// ``` + String _createToJsonFunctionSignature(String functionExpressionBody) { final buffer = StringBuffer(); final functionName = @@ -101,25 +118,82 @@ mixin EncodeHelper implements HelperCore { if (config.genericArgumentFactories) _writeGenericArgumentFactories(buffer); - buffer - ..write(') ') - ..writeln('=> {') - ..writeAll( - accessibleFields.map((field) { - final access = _fieldAccess(field); + buffer.write(') => $functionExpressionBody;'); - final keyExpression = safeNameAccess(field); - final valueExpression = _serializeField(field, access); + return buffer.toString(); + } - final maybeQuestion = _canWriteJsonWithoutNullCheck(field) ? '' : '?'; + /// Creates expression body for a function that serializes a union class. + /// + /// For example: + /// ```dart + /// switch (instance) { + /// final FirstSubtype instance => { + /// 'type': 'FirstSubtype', + /// ..._$FirstSubtypeToJson(instance), + /// }, + /// final SecondSubtype instance => { + /// 'type': 'SecondSubtype', + /// ..._$SecondSubtypeToJson(instance), + /// }, + /// } + /// ``` + String _createSealedFunctionExpressionBody() { + assert(element.isSealed); + + final implementations = sealedSubClasses(element); + + final discriminator = config.unionDiscriminator; + + String buildSingleImpl(ClassElement2 impl) { + final originalName = impl.name3!; + + final unionName = encodedName(config.unionRename, originalName); + + return ''' + final $originalName instance => { + '$discriminator': '$unionName', + ...${classPrefix(impl)}ToJson(instance), + }, +'''; + } - final keyValuePair = '$keyExpression: $maybeQuestion$valueExpression'; - return ' $keyValuePair,\n'; - }), - ) - ..writeln('};'); + final buffer = StringBuffer() + ..writeln('switch (instance) {') + ..writeAll(implementations.map(buildSingleImpl)) + ..writeln('}'); - yield buffer.toString(); + return buffer.toString(); + } + + /// Creates expression body for a function that serializes a class. + /// + /// For example: + /// ```dart + /// { + /// 'exampleField': instance.exampleField, + /// } + /// ``` + String _createFieldMapFunctionExpressionBody( + Set accessibleFields, + ) { + String buildSingleField(FieldElement2 field) { + final access = _fieldAccess(field); + + final keyExpression = safeNameAccess(field); + final valueExpression = _serializeField(field, access); + + final maybeQuestion = _canWriteJsonWithoutNullCheck(field) ? '' : '?'; + + return '$keyExpression: $maybeQuestion$valueExpression,'; + } + + final buffer = StringBuffer() + ..writeln('{') + ..writeAll(accessibleFields.map(buildSingleField)) + ..writeln('}'); + + return buffer.toString(); } void _writeGenericArgumentFactories(StringBuffer buffer) { diff --git a/json_serializable/lib/src/enum_utils.dart b/json_serializable/lib/src/enum_utils.dart index 77518c13d..678bd85ff 100644 --- a/json_serializable/lib/src/enum_utils.dart +++ b/json_serializable/lib/src/enum_utils.dart @@ -124,7 +124,7 @@ Object? _generateEntry({ ); } } else { - return encodedFieldName(jsonEnum.fieldRename, field.name3!); + return encodedName(jsonEnum.fieldRename, field.name3!); } } else { final reader = ConstantReader(annotation); @@ -153,7 +153,7 @@ JsonEnum _fromAnnotation(DartObject? dartObject) { final reader = ConstantReader(dartObject); return JsonEnum( alwaysCreate: reader.read('alwaysCreate').literalValue as bool, - fieldRename: readEnum(reader.read('fieldRename'), FieldRename.values)!, + fieldRename: readEnum(reader.read('fieldRename'), RenameType.values)!, valueField: reader.read('valueField').literalValue as String?, ); } diff --git a/json_serializable/lib/src/generator_helper.dart b/json_serializable/lib/src/generator_helper.dart index ff5577472..164639c4f 100644 --- a/json_serializable/lib/src/generator_helper.dart +++ b/json_serializable/lib/src/generator_helper.dart @@ -48,6 +48,84 @@ class GeneratorHelper extends HelperCore with EncodeHelper, DecodeHelper { ); } + final sealedSupersAndConfigs = sealedSuperClasses(element).map( + (superClass) => ( + classElement: superClass, + config: jsonSerializableConfig(superClass, _generator), + ), + ); + + if (sealedSupersAndConfigs.where((e) => e.config == null).firstOrNull + case final notAnnotated? when sealedSupersAndConfigs.isNotEmpty) { + throw InvalidGenerationSourceError( + 'The class `${element.displayName}` is annotated ' + 'with `JsonSerializable` but its sealed superclass ' + '`${notAnnotated.classElement.displayName}` is not annotated ' + 'with `JsonSerializable`.', + todo: 'Add `@JsonSerializable` annotation to the sealed class.', + element: element, + ); + } + + if ((sealedSupersAndConfigs.isNotEmpty || element.isSealed) && + config.genericArgumentFactories) { + throw InvalidGenerationSourceError( + 'The class `${element.displayName}` is annotated ' + 'with `JsonSerializable` field `genericArgumentFactories: true`. ' + '`genericArgumentFactories: true` is not supported for classes ' + 'that are sealed or have sealed superclasses.', + todo: + 'Remove the `genericArgumentFactories` option or ' + 'remove the `sealed` keyword from the class.', + element: element, + ); + } + + if (sealedSupersAndConfigs + .where( + (e) => e.config?.unionDiscriminator == config.unionDiscriminator, + ) + .firstOrNull + case final conflictingSuper? when element.isSealed) { + throw InvalidGenerationSource( + 'The classes `${conflictingSuper.classElement.displayName}` and ' + '`${element.displayName}` are nested sealed classes, but they have ' + 'the same discriminator `${config.unionDiscriminator}`.', + todo: + 'Rename one of the discriminators with `unionDiscriminator` ' + 'field of `@JsonSerializable`.', + element: element, + ); + } + + if (sealedSupersAndConfigs + .where((e) => e.config?.createToJson != config.createToJson) + .firstOrNull + case final diffSuper?) { + throw InvalidGenerationSourceError( + 'The class `${diffSuper.classElement.displayName}` is sealed but its ' + 'subclass `${element.displayName}` has a different ' + '`createToJson` option than the base class.', + element: element, + ); + } + + if (element.isSealed) { + sealedSubClasses(element).forEach((sub) { + final annotationConfig = jsonSerializableConfig(sub, _generator); + + if (annotationConfig == null) { + throw InvalidGenerationSourceError( + 'The class `${element.displayName}` is sealed but its ' + 'subclass `${sub.displayName}` is not annotated with ' + '`JsonSerializable`.', + todo: 'Add `@JsonSerializable` annotation to ${sub.displayName}.', + element: sub, + ); + } + }); + } + final sortedFields = createSortedFieldSet(element); // Used to keep track of why a field is ignored. Useful for providing @@ -114,6 +192,19 @@ class GeneratorHelper extends HelperCore with EncodeHelper, DecodeHelper { // by `_writeCtor`. ..fold({}, (Set set, fe) { final jsonKey = nameAccess(fe); + + if (sealedSupersAndConfigs + .where((e) => e.config?.unionDiscriminator == jsonKey) + .firstOrNull + case final conflict?) { + throw InvalidGenerationSourceError( + 'The JSON key `$jsonKey` is conflicting with the discriminator ' + 'of sealed superclass `${conflict.classElement.displayName}`', + todo: 'Rename the field or the discriminator.', + element: fe, + ); + } + if (!set.add(jsonKey)) { throw InvalidGenerationSourceError( 'More than one field has the JSON key for name "$jsonKey".', diff --git a/json_serializable/lib/src/helper_core.dart b/json_serializable/lib/src/helper_core.dart index 43a78303e..e80999a58 100644 --- a/json_serializable/lib/src/helper_core.dart +++ b/json_serializable/lib/src/helper_core.dart @@ -38,7 +38,7 @@ abstract class HelperCore { escapeDartString(nameAccess(field)); @protected - String get prefix => '_\$${element.name3!.nonPrivate}'; + String get prefix => classPrefix(element); /// Returns a [String] representing the type arguments that exist on /// [element]. diff --git a/json_serializable/lib/src/json_key_utils.dart b/json_serializable/lib/src/json_key_utils.dart index 4d4f0a4c4..e4dc9e5cd 100644 --- a/json_serializable/lib/src/json_key_utils.dart +++ b/json_serializable/lib/src/json_key_utils.dart @@ -307,7 +307,7 @@ KeyConfig _populateJsonKey( disallowNullValue, classAnnotation.includeIfNull, ), - name: name ?? encodedFieldName(classAnnotation.fieldRename, element.name3!), + name: name ?? encodedName(classAnnotation.fieldRename, element.name3!), readValueFunctionName: readValueFunctionName, required: required ?? false, unknownEnumValue: unknownEnumValue, diff --git a/json_serializable/lib/src/type_helpers/config_types.dart b/json_serializable/lib/src/type_helpers/config_types.dart index 80d258589..1b57a70c7 100644 --- a/json_serializable/lib/src/type_helpers/config_types.dart +++ b/json_serializable/lib/src/type_helpers/config_types.dart @@ -53,12 +53,14 @@ class ClassConfig { final bool createPerFieldToJson; final bool disallowUnrecognizedKeys; final bool explicitToJson; - final FieldRename fieldRename; + final RenameType fieldRename; final bool genericArgumentFactories; final bool ignoreUnannotated; final bool includeIfNull; final Map ctorParamDefaults; final List converters; + final String unionDiscriminator; + final RenameType unionRename; const ClassConfig({ required this.anyMap, @@ -75,6 +77,8 @@ class ClassConfig { required this.genericArgumentFactories, required this.ignoreUnannotated, required this.includeIfNull, + required this.unionDiscriminator, + required this.unionRename, this.converters = const [], this.ctorParamDefaults = const {}, }); @@ -108,6 +112,10 @@ class ClassConfig { disallowUnrecognizedKeys: config.disallowUnrecognizedKeys ?? ClassConfig.defaults.disallowUnrecognizedKeys, + unionDiscriminator: + config.unionDiscriminator ?? + ClassConfig.defaults.unionDiscriminator, + unionRename: config.unionRename ?? ClassConfig.defaults.unionRename, // TODO typeConverters = [] ); @@ -124,10 +132,12 @@ class ClassConfig { createPerFieldToJson: false, disallowUnrecognizedKeys: false, explicitToJson: false, - fieldRename: FieldRename.none, + fieldRename: RenameType.none, genericArgumentFactories: false, ignoreUnannotated: false, includeIfNull: true, + unionDiscriminator: 'type', + unionRename: RenameType.none, ); JsonSerializable toJsonSerializable() => JsonSerializable( @@ -145,6 +155,8 @@ class ClassConfig { genericArgumentFactories: genericArgumentFactories, fieldRename: fieldRename, disallowUnrecognizedKeys: disallowUnrecognizedKeys, + unionDiscriminator: unionDiscriminator, + unionRename: unionRename, // TODO typeConverters = [] ); } diff --git a/json_serializable/lib/src/utils.dart b/json_serializable/lib/src/utils.dart index f7a014ab1..28a047287 100644 --- a/json_serializable/lib/src/utils.dart +++ b/json_serializable/lib/src/utils.dart @@ -9,10 +9,12 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:source_gen/source_gen.dart'; import 'package:source_helper/source_helper.dart'; +import 'settings.dart'; import 'shared_checkers.dart'; import 'type_helpers/config_types.dart'; const _jsonKeyChecker = TypeChecker.fromRuntime(JsonKey); +const _jsonSerializableChecker = TypeChecker.fromRuntime(JsonSerializable); DartObject? _jsonKeyAnnotation(FieldElement2 element) => _jsonKeyChecker.firstAnnotationOf(element) ?? @@ -60,11 +62,13 @@ JsonSerializable _valueForAnnotation(ConstantReader reader) => JsonSerializable( disallowUnrecognizedKeys: reader.read('disallowUnrecognizedKeys').literalValue as bool?, explicitToJson: reader.read('explicitToJson').literalValue as bool?, - fieldRename: readEnum(reader.read('fieldRename'), FieldRename.values), + fieldRename: readEnum(reader.read('fieldRename'), RenameType.values), genericArgumentFactories: reader.read('genericArgumentFactories').literalValue as bool?, ignoreUnannotated: reader.read('ignoreUnannotated').literalValue as bool?, includeIfNull: reader.read('includeIfNull').literalValue as bool?, + unionDiscriminator: reader.read('unionDiscriminator').literalValue as String?, + unionRename: readEnum(reader.read('unionRename'), RenameType.values), ); /// Returns a [ClassConfig] with values from the [JsonSerializable] @@ -122,6 +126,9 @@ ClassConfig mergeConfig( includeIfNull: annotation.includeIfNull ?? config.includeIfNull, ctorParamDefaults: paramDefaultValueMap, converters: converters.isNull ? const [] : converters.listValue, + unionDiscriminator: + annotation.unionDiscriminator ?? config.unionDiscriminator, + unionRename: annotation.unionRename ?? config.unionRename, ); } @@ -162,6 +169,61 @@ ConstructorElement2 constructorByName(ClassElement2 classElement, String name) { return ctor; } +/// Given a [ClassElement2] that is a sealed class, returns all the +/// direct subclasses of the given sealed class, excluding any +/// indirect subclasses (ie. subclasses of subclasses). +/// +/// Otherwise, returns an empty iterable. +Iterable sealedSubClasses(ClassElement2 maybeSealedSuperClass) { + if (maybeSealedSuperClass case final sc when sc.isSealed) { + return LibraryReader( + sc.library2, + ).allElements.whereType().where( + (e) => e.interfaces.contains(sc.thisType) || e.supertype?.element3 == sc, + ); + } + + return const Iterable.empty(); +} + +/// Given a [ClassElement2] that is a subclass of sealed classes, returns +/// all of the sealed superclasses, including all indirect superclasses +/// (ie. superclasses of superclasses) +/// +/// Otherwise, returns an empty iterable. +Iterable sealedSuperClasses( + ClassElement2 maybeSealedImplementation, +) => maybeSealedImplementation.allSupertypes + .map((type) => type.element3) + .whereType() + .where((element) => element.isSealed); + +/// Given a [ClassElement2] that is annotated with `@JsonSerializable`, returns +/// the annotation config merged with build runner config and defaults. +/// +/// Otherwise, returns `null`. +ClassConfig? jsonSerializableConfig( + ClassElement2 maybeAnnotatedElement, + Settings generator, +) { + final maybeSuperAnnotation = _jsonSerializableChecker.firstAnnotationOfExact( + maybeAnnotatedElement, + throwOnUnresolved: false, + ); + + if (maybeSuperAnnotation case final superAnnotation?) { + final annotationReader = ConstantReader(superAnnotation); + + return mergeConfig( + generator.config, + annotationReader, + classElement: maybeAnnotatedElement, + ); + } + + return null; +} + /// If [targetType] is an enum, returns the [FieldElement2] instances associated /// with its values. /// @@ -188,15 +250,17 @@ extension DartTypeExtension on DartType { String ifNullOrElse(String test, String ifNull, String ifNotNull) => '$test == null ? $ifNull : $ifNotNull'; -String encodedFieldName(FieldRename fieldRename, String declaredName) => +String encodedName(RenameType fieldRename, String declaredName) => switch (fieldRename) { - FieldRename.none => declaredName, - FieldRename.snake => declaredName.snake, - FieldRename.screamingSnake => declaredName.snake.toUpperCase(), - FieldRename.kebab => declaredName.kebab, - FieldRename.pascal => declaredName.pascal, + RenameType.none => declaredName, + RenameType.snake => declaredName.snake, + RenameType.screamingSnake => declaredName.snake.toUpperCase(), + RenameType.kebab => declaredName.kebab, + RenameType.pascal => declaredName.pascal, }; +String classPrefix(ClassElement2 element) => '_\$${element.name3!.nonPrivate}'; + /// Return the Dart code presentation for the given [type]. /// /// This function is intentionally limited, and does not support all possible diff --git a/json_serializable/pubspec.yaml b/json_serializable/pubspec.yaml index d2184e4b9..62719fea6 100644 --- a/json_serializable/pubspec.yaml +++ b/json_serializable/pubspec.yaml @@ -1,5 +1,5 @@ name: json_serializable -version: 6.10.0 +version: 6.11.0-wip description: >- Automatically generate code for converting to and from JSON by annotating Dart classes. @@ -19,11 +19,11 @@ dependencies: async: ^2.10.0 build: ^3.0.0 build_config: ^1.1.0 - dart_style: '>=2.3.7 <4.0.0' + dart_style: ^3.1.0 # Use a tight version constraint to ensure that a constraint on # `json_annotation` properly constrains all features it provides. - json_annotation: '>=4.9.0 <4.10.0' + json_annotation: '>=4.10.0-wip <4.11.0' meta: ^1.14.0 path: ^1.9.0 pub_semver: ^2.1.4 diff --git a/json_serializable/test/config_test.dart b/json_serializable/test/config_test.dart index b29ffac12..c9f135c60 100644 --- a/json_serializable/test/config_test.dart +++ b/json_serializable/test/config_test.dart @@ -43,6 +43,12 @@ void main() { ); for (var entry in generatorConfigDefaultJson.entries) { + expect( + generatorConfigNonDefaultJson[entry.key], + isNotNull, + reason: 'should have explicitly set non default value', + ); + expect( generatorConfigNonDefaultJson, containsPair(entry.key, isNot(entry.value)), @@ -91,7 +97,7 @@ void main() { configMap.keys, unorderedEquals(generatorConfigDefaultJson.keys), reason: - 'All supported keys are documented. ' + 'All supported keys are not documented. ' 'Did you forget to change README.md?', ); @@ -133,6 +139,12 @@ void main() { 'field_rename' => '`42` is not one of the supported values: none, kebab, snake, ' 'pascal, screamingSnake', + 'union_rename' => + '`42` is not one of the supported values: none, kebab, snake, ' + 'pascal, screamingSnake', + 'union_discriminator' => + "type 'int' is not a subtype of type 'String?' in type " + 'cast', 'constructor' => "type 'int' is not a subtype of type 'String?' in type " 'cast', @@ -175,4 +187,6 @@ const _invalidConfig = { 'generic_argument_factories': 42, 'ignore_unannotated': 42, 'include_if_null': 42, + 'union_discriminator': 42, + 'union_rename': 42, }; diff --git a/json_serializable/test/integration/create_per_field_to_json_example.dart b/json_serializable/test/integration/create_per_field_to_json_example.dart index d086253f3..93a11bed0 100644 --- a/json_serializable/test/integration/create_per_field_to_json_example.dart +++ b/json_serializable/test/integration/create_per_field_to_json_example.dart @@ -73,7 +73,7 @@ typedef GenericFactoryPerFieldToJson = _$GenericFactoryPerFieldToJson; @JsonSerializable( createPerFieldToJson: true, - fieldRename: FieldRename.kebab, + fieldRename: RenameType.kebab, createFactory: false, ) class _PrivateModel { diff --git a/json_serializable/test/integration/field_map_example.dart b/json_serializable/test/integration/field_map_example.dart index 3eef44693..acc81ee43 100644 --- a/json_serializable/test/integration/field_map_example.dart +++ b/json_serializable/test/integration/field_map_example.dart @@ -2,7 +2,7 @@ import 'package:json_annotation/json_annotation.dart'; part 'field_map_example.g.dart'; -@JsonSerializable(createFieldMap: true, fieldRename: FieldRename.kebab) +@JsonSerializable(createFieldMap: true, fieldRename: RenameType.kebab) class Model { Model({required this.firstName, required this.lastName, this.ignoredName}); @@ -25,7 +25,7 @@ const modelFieldMap = _$ModelFieldMap; @JsonSerializable( createFieldMap: true, - fieldRename: FieldRename.kebab, + fieldRename: RenameType.kebab, createFactory: false, ) class _PrivateModel { diff --git a/json_serializable/test/integration/integration_test.dart b/json_serializable/test/integration/integration_test.dart index 9cc5d0948..6fc4e6657 100644 --- a/json_serializable/test/integration/integration_test.dart +++ b/json_serializable/test/integration/integration_test.dart @@ -13,6 +13,7 @@ import 'json_enum_example.dart'; import 'json_keys_example.dart' as js_keys; import 'json_test_common.dart' show Category, Platform, StatusCode; import 'json_test_example.dart'; +import 'sealed_class_examples.dart'; Matcher _throwsArgumentError(Object matcher) => throwsA(isArgumentError.having((e) => e.message, 'message', matcher)); @@ -465,6 +466,88 @@ void main() { }); }); + group('Vehicle', () { + void roundTripVehicle(Vehicle v) { + roundTripObject(v, Vehicle.fromJson); + } + + test('Car', () { + roundTripVehicle(Car(numberOfDoors: 4, vehicleID: 'vehicle-123')); + }); + + test('Bicycle', () { + roundTripVehicle(Bicycle(hasBell: true, vehicleID: 'vehicle-456')); + }); + }); + + group('Delivery', () { + void roundTripDelivery(Delivery d) { + roundTripObject(d, Delivery.fromJson); + } + + test('DroneDelivery', () { + roundTripDelivery(DroneDelivery(droneModel: 1, deliveryID: 789)); + }); + + test('TruckDelivery', () { + roundTripDelivery( + TruckDelivery(weightCapacity: 3000.0, deliveryID: 1011), + ); + }); + }); + + group('Container', () { + void roundTripContainer(Container c) { + roundTripObject(c, Container.fromJson); + } + + test('Box with nested items', () { + roundTripContainer( + Box( + containerID: 'box-321', + items: [ + Product(name: 'Product-X', itemID: 'item-654'), + Equipment( + label: 'Type-Y', + itemID: 'item-987', + nestedContainer: Parcel( + containerID: 'nested-parcel-222', + items: [Product(name: 'Product-Y', itemID: 'item-876')], + ), + ), + ], + ), + ); + }); + + test('Parcel without items', () { + roundTripContainer(Parcel(containerID: 'parcel-111')); + }); + }); + + group('Transportable and Trackable', () { + void roundTripTransportable(Transportable t) { + roundTripObject(t, Transportable.fromJson); + } + + void roundTripTrackable(Trackable t) { + roundTripObject(t, Trackable.fromJson); + } + + test('Ship', () { + roundTripTransportable(Ship(cargoCapacity: 5000.0)); + }); + + test('GPSDevice', () { + roundTripTrackable(GPSDevice(serialNumber: 'gps-12345')); + }); + + test('Package', () { + roundTripTransportable(Package(label: 'Package-123', weight: 2.5)); + roundTripTrackable(Package(label: 'Package-123', weight: 2.5)); + }); + }); + test('Issue1226Regression', () { final instance = Issue1226Regression(durationType: null); expect(instance.toJson(), isEmpty); diff --git a/json_serializable/test/integration/json_enum_example.dart b/json_serializable/test/integration/json_enum_example.dart index f36159cc1..85cba625e 100644 --- a/json_serializable/test/integration/json_enum_example.dart +++ b/json_serializable/test/integration/json_enum_example.dart @@ -16,7 +16,7 @@ enum StandAloneEnum { Iterable get standAloneEnumValues => _$StandAloneEnumEnumMap.values; -@JsonEnum(alwaysCreate: true, fieldRename: FieldRename.kebab) +@JsonEnum(alwaysCreate: true, fieldRename: RenameType.kebab) enum DayType { noGood, rotten, veryBad } Iterable get dayTypeEnumValues => _$DayTypeEnumMap.values; diff --git a/json_serializable/test/integration/json_keys_example.dart b/json_serializable/test/integration/json_keys_example.dart index 985979d28..a59136281 100644 --- a/json_serializable/test/integration/json_keys_example.dart +++ b/json_serializable/test/integration/json_keys_example.dart @@ -2,7 +2,7 @@ import 'package:json_annotation/json_annotation.dart'; part 'json_keys_example.g.dart'; -@JsonSerializable(createJsonKeys: true, fieldRename: FieldRename.kebab) +@JsonSerializable(createJsonKeys: true, fieldRename: RenameType.kebab) class Model { Model({required this.firstName, required this.lastName, this.ignoredName}); diff --git a/json_serializable/test/integration/json_test_common.dart b/json_serializable/test/integration/json_test_common.dart index 95372161e..b0f821c14 100644 --- a/json_serializable/test/integration/json_test_common.dart +++ b/json_serializable/test/integration/json_test_common.dart @@ -6,7 +6,7 @@ import 'dart:collection'; import 'package:json_annotation/json_annotation.dart'; -@JsonEnum(fieldRename: FieldRename.kebab) +@JsonEnum(fieldRename: RenameType.kebab) enum Category { top, bottom, diff --git a/json_serializable/test/integration/sealed_class_examples.dart b/json_serializable/test/integration/sealed_class_examples.dart new file mode 100644 index 000000000..bb54279d9 --- /dev/null +++ b/json_serializable/test/integration/sealed_class_examples.dart @@ -0,0 +1,333 @@ +import 'dart:convert'; + +import 'package:json_annotation/json_annotation.dart'; + +import '../test_utils.dart'; + +part 'sealed_class_examples.g.dart'; + +@JsonSerializable() +sealed class Vehicle { + final String vehicleID; + + Vehicle({required this.vehicleID}); + + factory Vehicle.fromJson(Map json) => + _$VehicleFromJson(json); + + Map toJson() => _$VehicleToJson(this); +} + +@JsonSerializable() +class Car extends Vehicle { + final int numberOfDoors; + + Car({required this.numberOfDoors, required super.vehicleID}); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Car && + runtimeType == other.runtimeType && + numberOfDoors == other.numberOfDoors && + vehicleID == other.vehicleID; + + @override + int get hashCode => jsonEncode(this).hashCode; +} + +@JsonSerializable() +class Bicycle extends Vehicle { + final bool hasBell; + + Bicycle({required this.hasBell, required super.vehicleID}); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Bicycle && + runtimeType == other.runtimeType && + hasBell == other.hasBell && + vehicleID == other.vehicleID; + + @override + int get hashCode => jsonEncode(this).hashCode; +} + +@JsonSerializable( + unionDiscriminator: 'delivery_type', + unionRename: RenameType.snake, +) +sealed class Delivery { + final int deliveryID; + + Delivery({required this.deliveryID}); + + factory Delivery.fromJson(Map json) => + _$DeliveryFromJson(json); + + Map toJson() => _$DeliveryToJson(this); +} + +@JsonSerializable() +class DroneDelivery extends Delivery { + final int droneModel; + + DroneDelivery({required this.droneModel, required super.deliveryID}); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is DroneDelivery && + runtimeType == other.runtimeType && + droneModel == other.droneModel && + deliveryID == other.deliveryID; + + @override + int get hashCode => jsonEncode(this).hashCode; +} + +@JsonSerializable() +class TruckDelivery extends Delivery { + final double weightCapacity; + + TruckDelivery({required this.weightCapacity, required super.deliveryID}); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is TruckDelivery && + runtimeType == other.runtimeType && + weightCapacity == other.weightCapacity && + deliveryID == other.deliveryID; + + @override + int get hashCode => jsonEncode(this).hashCode; +} + +@JsonSerializable(unionDiscriminator: 'organization') +sealed class Organization { + final String name; + + Organization({required this.name}); + + factory Organization.fromJson(Map json) => + _$OrganizationFromJson(json); + + Map 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 json) => + _$DepartmentFromJson(json); +} + +@JsonSerializable() +class Team extends Department { + final String teamLead; + + Team({ + required this.teamLead, + required super.departmentHead, + required super.name, + }); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Team && + runtimeType == other.runtimeType && + teamLead == other.teamLead && + departmentHead == other.departmentHead && + name == other.name; + + @override + int get hashCode => jsonEncode(this).hashCode; +} + +@JsonSerializable() +sealed class Transportable { + const Transportable(); + + factory Transportable.fromJson(Map json) => + _$TransportableFromJson(json); + + Map toJson(); +} + +@JsonSerializable() +sealed class Trackable { + const Trackable(); + + factory Trackable.fromJson(Map json) => + _$TrackableFromJson(json); + + Map toJson(); +} + +@JsonSerializable() +class Ship implements Transportable { + final double cargoCapacity; + + Ship({required this.cargoCapacity}); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Ship && + runtimeType == other.runtimeType && + cargoCapacity == other.cargoCapacity; + + @override + int get hashCode => jsonEncode(this).hashCode; + + @override + Map toJson() => _$TransportableToJson(this); +} + +@JsonSerializable() +class GPSDevice implements Trackable { + final String serialNumber; + + GPSDevice({required this.serialNumber}); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is GPSDevice && + runtimeType == other.runtimeType && + serialNumber == other.serialNumber; + + @override + int get hashCode => jsonEncode(this).hashCode; + + @override + Map toJson() => _$TrackableToJson(this); +} + +@JsonSerializable() +class Package implements Transportable, Trackable { + final String label; + final double weight; + + Package({required this.label, required this.weight}); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Package && + runtimeType == other.runtimeType && + label == other.label && + weight == other.weight; + + @override + int get hashCode => jsonEncode(this).hashCode; + + @override + Map toJson() => _$TrackableToJson(this); +} + +@JsonSerializable() +sealed class Container { + final String containerID; + final List items; + + Container({required this.containerID, this.items = const []}); + + factory Container.fromJson(Map json) => + _$ContainerFromJson(json); + + Map toJson() => _$ContainerToJson(this); +} + +@JsonSerializable() +sealed class StorageItem { + final String itemID; + final Container? nestedContainer; + + StorageItem({required this.itemID, this.nestedContainer}); + + factory StorageItem.fromJson(Map json) => + _$StorageItemFromJson(json); + + Map toJson() => _$StorageItemToJson(this); +} + +@JsonSerializable() +class Box extends Container { + Box({required super.containerID, super.items}); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Box && + runtimeType == other.runtimeType && + containerID == other.containerID && + deepEquals(items, other.items); + + @override + int get hashCode => jsonEncode(this).hashCode; +} + +@JsonSerializable() +class Parcel extends Container { + Parcel({required super.containerID, super.items}); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Parcel && + runtimeType == other.runtimeType && + containerID == other.containerID && + deepEquals(items, items); + + @override + int get hashCode => jsonEncode(this).hashCode; +} + +@JsonSerializable() +class Product extends StorageItem { + final String name; + + Product({required this.name, required super.itemID, super.nestedContainer}); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Product && + runtimeType == other.runtimeType && + name == other.name && + itemID == other.itemID && + nestedContainer == other.nestedContainer; + + @override + int get hashCode => jsonEncode(this).hashCode; +} + +@JsonSerializable() +class Equipment extends StorageItem { + final String label; + + Equipment({ + required this.label, + required super.itemID, + super.nestedContainer, + }); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Equipment && + runtimeType == other.runtimeType && + label == other.label && + itemID == other.itemID && + nestedContainer == other.nestedContainer; + + @override + int get hashCode => jsonEncode(this).hashCode; +} diff --git a/json_serializable/test/integration/sealed_class_examples.g.dart b/json_serializable/test/integration/sealed_class_examples.g.dart new file mode 100644 index 000000000..598dbb8a9 --- /dev/null +++ b/json_serializable/test/integration/sealed_class_examples.g.dart @@ -0,0 +1,294 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: lines_longer_than_80_chars, text_direction_code_point_in_literal, inference_failure_on_function_invocation, inference_failure_on_collection_literal + +part of 'sealed_class_examples.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Vehicle _$VehicleFromJson(Map json) => switch (json['type']) { + 'Car' => _$CarFromJson(json), + 'Bicycle' => _$BicycleFromJson(json), + _ => throw UnrecognizedUnionTypeException('${json['type']}', Vehicle, json), +}; + +Map _$VehicleToJson(Vehicle instance) => switch (instance) { + final Car instance => {'type': 'Car', ..._$CarToJson(instance)}, + final Bicycle instance => {'type': 'Bicycle', ..._$BicycleToJson(instance)}, +}; + +Car _$CarFromJson(Map json) => Car( + numberOfDoors: (json['numberOfDoors'] as num).toInt(), + vehicleID: json['vehicleID'] as String, +); + +Map _$CarToJson(Car instance) => { + 'vehicleID': instance.vehicleID, + 'numberOfDoors': instance.numberOfDoors, +}; + +Bicycle _$BicycleFromJson(Map json) => Bicycle( + hasBell: json['hasBell'] as bool, + vehicleID: json['vehicleID'] as String, +); + +Map _$BicycleToJson(Bicycle instance) => { + 'vehicleID': instance.vehicleID, + 'hasBell': instance.hasBell, +}; + +Delivery _$DeliveryFromJson(Map json) => + switch (json['delivery_type']) { + 'drone_delivery' => _$DroneDeliveryFromJson(json), + 'truck_delivery' => _$TruckDeliveryFromJson(json), + _ => throw UnrecognizedUnionTypeException( + '${json['delivery_type']}', + Delivery, + json, + ), + }; + +Map _$DeliveryToJson(Delivery instance) => switch (instance) { + final DroneDelivery instance => { + 'delivery_type': 'drone_delivery', + ..._$DroneDeliveryToJson(instance), + }, + final TruckDelivery instance => { + 'delivery_type': 'truck_delivery', + ..._$TruckDeliveryToJson(instance), + }, +}; + +DroneDelivery _$DroneDeliveryFromJson(Map json) => + DroneDelivery( + droneModel: (json['droneModel'] as num).toInt(), + deliveryID: (json['deliveryID'] as num).toInt(), + ); + +Map _$DroneDeliveryToJson(DroneDelivery instance) => + { + 'deliveryID': instance.deliveryID, + 'droneModel': instance.droneModel, + }; + +TruckDelivery _$TruckDeliveryFromJson(Map json) => + TruckDelivery( + weightCapacity: (json['weightCapacity'] as num).toDouble(), + deliveryID: (json['deliveryID'] as num).toInt(), + ); + +Map _$TruckDeliveryToJson(TruckDelivery instance) => + { + 'deliveryID': instance.deliveryID, + 'weightCapacity': instance.weightCapacity, + }; + +Organization _$OrganizationFromJson(Map json) => + switch (json['organization']) { + 'Department' => _$DepartmentFromJson(json), + _ => throw UnrecognizedUnionTypeException( + '${json['organization']}', + Organization, + json, + ), + }; + +Map _$OrganizationToJson(Organization instance) => + switch (instance) { + final Department instance => { + 'organization': 'Department', + ..._$DepartmentToJson(instance), + }, + }; + +Department _$DepartmentFromJson(Map json) => + switch (json['department']) { + 'Team' => _$TeamFromJson(json), + _ => throw UnrecognizedUnionTypeException( + '${json['department']}', + Department, + json, + ), + }; + +Map _$DepartmentToJson(Department instance) => + switch (instance) { + final Team instance => {'department': 'Team', ..._$TeamToJson(instance)}, + }; + +Team _$TeamFromJson(Map json) => Team( + teamLead: json['teamLead'] as String, + departmentHead: json['departmentHead'] as String, + name: json['name'] as String, +); + +Map _$TeamToJson(Team instance) => { + 'name': instance.name, + 'departmentHead': instance.departmentHead, + 'teamLead': instance.teamLead, +}; + +Transportable _$TransportableFromJson(Map json) => + switch (json['type']) { + 'Ship' => _$ShipFromJson(json), + 'Package' => _$PackageFromJson(json), + _ => throw UnrecognizedUnionTypeException( + '${json['type']}', + Transportable, + json, + ), + }; + +Map _$TransportableToJson(Transportable instance) => + switch (instance) { + final Ship instance => {'type': 'Ship', ..._$ShipToJson(instance)}, + final Package instance => { + 'type': 'Package', + ..._$PackageToJson(instance), + }, + }; + +Trackable _$TrackableFromJson(Map json) => + switch (json['type']) { + 'GPSDevice' => _$GPSDeviceFromJson(json), + 'Package' => _$PackageFromJson(json), + _ => throw UnrecognizedUnionTypeException( + '${json['type']}', + Trackable, + json, + ), + }; + +Map _$TrackableToJson(Trackable instance) => + switch (instance) { + final GPSDevice instance => { + 'type': 'GPSDevice', + ..._$GPSDeviceToJson(instance), + }, + final Package instance => { + 'type': 'Package', + ..._$PackageToJson(instance), + }, + }; + +Ship _$ShipFromJson(Map json) => + Ship(cargoCapacity: (json['cargoCapacity'] as num).toDouble()); + +Map _$ShipToJson(Ship instance) => { + 'cargoCapacity': instance.cargoCapacity, +}; + +GPSDevice _$GPSDeviceFromJson(Map json) => + GPSDevice(serialNumber: json['serialNumber'] as String); + +Map _$GPSDeviceToJson(GPSDevice instance) => { + 'serialNumber': instance.serialNumber, +}; + +Package _$PackageFromJson(Map json) => Package( + label: json['label'] as String, + weight: (json['weight'] as num).toDouble(), +); + +Map _$PackageToJson(Package instance) => { + 'label': instance.label, + 'weight': instance.weight, +}; + +Container _$ContainerFromJson(Map json) => + switch (json['type']) { + 'Box' => _$BoxFromJson(json), + 'Parcel' => _$ParcelFromJson(json), + _ => throw UnrecognizedUnionTypeException( + '${json['type']}', + Container, + json, + ), + }; + +Map _$ContainerToJson(Container instance) => + switch (instance) { + final Box instance => {'type': 'Box', ..._$BoxToJson(instance)}, + final Parcel instance => {'type': 'Parcel', ..._$ParcelToJson(instance)}, + }; + +StorageItem _$StorageItemFromJson(Map json) => + switch (json['type']) { + 'Product' => _$ProductFromJson(json), + 'Equipment' => _$EquipmentFromJson(json), + _ => throw UnrecognizedUnionTypeException( + '${json['type']}', + StorageItem, + json, + ), + }; + +Map _$StorageItemToJson(StorageItem instance) => + switch (instance) { + final Product instance => { + 'type': 'Product', + ..._$ProductToJson(instance), + }, + final Equipment instance => { + 'type': 'Equipment', + ..._$EquipmentToJson(instance), + }, + }; + +Box _$BoxFromJson(Map json) => Box( + containerID: json['containerID'] as String, + items: + (json['items'] as List?) + ?.map((e) => StorageItem.fromJson(e as Map)) + .toList() ?? + const [], +); + +Map _$BoxToJson(Box instance) => { + 'containerID': instance.containerID, + 'items': instance.items, +}; + +Parcel _$ParcelFromJson(Map json) => Parcel( + containerID: json['containerID'] as String, + items: + (json['items'] as List?) + ?.map((e) => StorageItem.fromJson(e as Map)) + .toList() ?? + const [], +); + +Map _$ParcelToJson(Parcel instance) => { + 'containerID': instance.containerID, + 'items': instance.items, +}; + +Product _$ProductFromJson(Map json) => Product( + name: json['name'] as String, + itemID: json['itemID'] as String, + nestedContainer: json['nestedContainer'] == null + ? null + : Container.fromJson(json['nestedContainer'] as Map), +); + +Map _$ProductToJson(Product instance) => { + 'itemID': instance.itemID, + 'nestedContainer': instance.nestedContainer, + 'name': instance.name, +}; + +Equipment _$EquipmentFromJson(Map json) => Equipment( + label: json['label'] as String, + itemID: json['itemID'] as String, + nestedContainer: json['nestedContainer'] == null + ? null + : Container.fromJson(json['nestedContainer'] as Map), +); + +Map _$EquipmentToJson(Equipment instance) => { + 'itemID': instance.itemID, + 'nestedContainer': instance.nestedContainer, + 'label': instance.label, +}; diff --git a/json_serializable/test/json_serializable_test.dart b/json_serializable/test/json_serializable_test.dart index b6ff36789..841b9d78c 100644 --- a/json_serializable/test/json_serializable_test.dart +++ b/json_serializable/test/json_serializable_test.dart @@ -122,11 +122,72 @@ const _expectedAnnotatedTests = { 'Reproduce869NullableGenericTypeWithDefault', 'SameCtorAndJsonKeyDefaultValue', 'SetSupport', + 'SubFourMultipleImpl', + 'SubNestedOneOne', + 'SubNestedOneTwo', + 'SubNestedTwoOne', + 'SubNestedTwoTwo', + 'SubOneMultipleImpl', + 'SubOneSimpleSealedClass', + 'SubOneSimpleSealedClassWithChangedDiscriminator', + 'SubOneSimpleSealedClassWithChangedDiscriminatorAndUnionRename', + 'SubOneSimpleSealedClassWithChangedUnionRename', + 'SubSimpleSealedClassWithoutToJson', + 'SubThreeMultipleImpl', + 'SubTwoMultipleImpl', + 'SubTwoSimpleSealedClass', + 'SubTwoSimpleSealedClassWithChangedDiscriminator', + 'SubTwoSimpleSealedClassWithChangedDiscriminatorAndUnionRename', + 'SubTwoSimpleSealedClassWithChangedUnionRename', 'SubType', 'SubTypeWithAnnotatedFieldOverrideExtends', 'SubTypeWithAnnotatedFieldOverrideExtendsWithOverrides', 'SubTypeWithAnnotatedFieldOverrideImplements', + 'SubUnionRenameKebab', + 'SubUnionRenameNone', + 'SubUnionRenamePascal', + 'SubUnionRenameScreamingSnake', + 'SubUnionRenameSnake', + 'SubWithConflictingDiscriminatorWithDefaultNameExt', + 'SubWithConflictingDiscriminatorWithDefaultNameImpl', + 'SubWithConflictingDiscriminatorWithFieldRenameExt', + 'SubWithConflictingDiscriminatorWithFieldRenameImpl', + 'SubWithConflictingDiscriminatorWithJsonKeyExt', + 'SubWithConflictingDiscriminatorWithJsonKeyImpl', + 'SubWithSubAndSuperGenericArgumentFactoriesExt', + 'SubWithSubAndSuperGenericArgumentFactoriesImpl', + 'SubWithSubGenericArgumentFactoriesExt', + 'SubWithSubGenericArgumentFactoriesImpl', + 'SubWithToJsonAndSuperWithoutToJsonExt', + 'SubWithToJsonAndSuperWithoutToJsonImpl', + 'SubWithoutSuperJsonSerializableAnnotationExt', + 'SubWithoutSuperJsonSerializableAnnotationImpl', + 'SubWithoutToJsonAndSuperWithToJsonExt', + 'SubWithoutToJsonAndSuperWithToJsonImpl', 'SubclassedJsonKey', + 'SuperMultipleImplOne', + 'SuperMultipleImplTwo', + 'SuperNestedOneOne', + 'SuperNestedOneTwo', + 'SuperNestedTwoOne', + 'SuperNestedTwoTwo', + 'SuperSimpleSealedClass', + 'SuperSimpleSealedClassWithChangedDiscriminator', + 'SuperSimpleSealedClassWithChangedDiscriminatorAndUnionRename', + 'SuperSimpleSealedClassWithChangedUnionRename', + 'SuperSimpleSealedClassWithoutToJson', + 'SuperSuperNestedOne', + 'SuperSuperNestedTwo', + 'SuperSuperSuperNested', + 'SuperUnionRenameKebab', + 'SuperUnionRenameNone', + 'SuperUnionRenamePascal', + 'SuperUnionRenameScreamingSnake', + 'SuperUnionRenameSnake', + 'SuperWithConflictingNestedDiscriminator', + 'SuperWithGenericArgumentFactories', + 'SuperWithSubExtWithoutJsonSerializableAnnotation', + 'SuperWithSubImplWithoutJsonSerializableAnnotation', 'TearOffFromJsonClass', 'ToJsonNullableFalseIncludeIfNullFalse', 'TypedConvertMethods', diff --git a/json_serializable/test/shared_config.dart b/json_serializable/test/shared_config.dart index 510250175..6ffec9555 100644 --- a/json_serializable/test/shared_config.dart +++ b/json_serializable/test/shared_config.dart @@ -24,9 +24,11 @@ final generatorConfigNonDefaultJson = Map.unmodifiable( createPerFieldToJson: true, disallowUnrecognizedKeys: true, explicitToJson: true, - fieldRename: FieldRename.kebab, + fieldRename: RenameType.kebab, ignoreUnannotated: true, includeIfNull: false, genericArgumentFactories: true, + unionDiscriminator: 'runtimeType', + unionRename: RenameType.kebab, ).toJson(), ); diff --git a/json_serializable/test/src/_json_serializable_test_input.dart b/json_serializable/test/src/_json_serializable_test_input.dart index 09deed525..3f0b39242 100644 --- a/json_serializable/test/src/_json_serializable_test_input.dart +++ b/json_serializable/test/src/_json_serializable_test_input.dart @@ -10,6 +10,7 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:source_gen_test/annotations.dart'; part 'checked_test_input.dart'; +part 'conflicting_discriminator_input.dart'; part 'constants_copy.dart'; part 'core_subclass_type_input.dart'; part 'default_value_input.dart'; @@ -18,8 +19,12 @@ part 'generic_test_input.dart'; part 'inheritance_test_input.dart'; part 'json_converter_test_input.dart'; part 'map_key_variety_test_input.dart'; +part 'mismatching_config_input.dart'; +part 'missing_annotation_input.dart'; +part 'sealed_test_input.dart'; part 'setter_test_input.dart'; part 'to_from_json_test_input.dart'; +part 'union_namer_input.dart'; part 'unknown_enum_value_test_input.dart'; @ShouldThrow('`@JsonSerializable` can only be used on classes.') diff --git a/json_serializable/test/src/conflicting_discriminator_input.dart b/json_serializable/test/src/conflicting_discriminator_input.dart new file mode 100644 index 000000000..4bbef9037 --- /dev/null +++ b/json_serializable/test/src/conflicting_discriminator_input.dart @@ -0,0 +1,119 @@ +// @dart=3.8 + +part of '_json_serializable_test_input.dart'; + +@JsonSerializable(unionDiscriminator: 'this_will_conflict') +sealed class SuperWithConflictingSnakeCaseDiscriminator {} + +@ShouldThrow( + 'The JSON key `this_will_conflict` is conflicting with the discriminator ' + 'of sealed superclass `SuperWithConflictingSnakeCaseDiscriminator`', + todo: 'Rename the field or the discriminator.', + element: 'thisWillConflict', +) +@JsonSerializable(fieldRename: RenameType.snake) +class SubWithConflictingDiscriminatorWithFieldRenameExt + extends SuperWithConflictingSnakeCaseDiscriminator { + final String thisWillConflict; + + SubWithConflictingDiscriminatorWithFieldRenameExt({ + required this.thisWillConflict, + }); +} + +@ShouldThrow( + 'The JSON key `this_will_conflict` is conflicting with the discriminator ' + 'of sealed superclass `SuperWithConflictingSnakeCaseDiscriminator`', + todo: 'Rename the field or the discriminator.', + element: 'thisWillConflict', +) +@JsonSerializable(fieldRename: RenameType.snake) +class SubWithConflictingDiscriminatorWithFieldRenameImpl + implements SuperWithConflictingSnakeCaseDiscriminator { + final String thisWillConflict; + + SubWithConflictingDiscriminatorWithFieldRenameImpl({ + required this.thisWillConflict, + }); +} + +@ShouldThrow( + 'The JSON key `this_will_conflict` is conflicting with the discriminator ' + 'of sealed superclass `SuperWithConflictingSnakeCaseDiscriminator`', + todo: 'Rename the field or the discriminator.', + element: 'conflict', +) +@JsonSerializable() +class SubWithConflictingDiscriminatorWithJsonKeyExt + extends SuperWithConflictingSnakeCaseDiscriminator { + @JsonKey(name: 'this_will_conflict') + final String conflict; + + SubWithConflictingDiscriminatorWithJsonKeyExt({required this.conflict}); +} + +@ShouldThrow( + 'The JSON key `this_will_conflict` is conflicting with the discriminator ' + 'of sealed superclass `SuperWithConflictingSnakeCaseDiscriminator`', + todo: 'Rename the field or the discriminator.', + element: 'conflict', +) +@JsonSerializable() +class SubWithConflictingDiscriminatorWithJsonKeyImpl + implements SuperWithConflictingSnakeCaseDiscriminator { + @JsonKey(name: 'this_will_conflict') + final String conflict; + + SubWithConflictingDiscriminatorWithJsonKeyImpl({required this.conflict}); +} + +@JsonSerializable(unionDiscriminator: 'conflict') +sealed class SuperWithConflictingDefaultCaseDiscriminator {} + +@ShouldThrow( + 'The JSON key `conflict` is conflicting with the discriminator ' + 'of sealed superclass `SuperWithConflictingDefaultCaseDiscriminator`', + todo: 'Rename the field or the discriminator.', + element: 'conflict', +) +@JsonSerializable() +class SubWithConflictingDiscriminatorWithDefaultNameExt + extends SuperWithConflictingDefaultCaseDiscriminator { + final String conflict; + + SubWithConflictingDiscriminatorWithDefaultNameExt({required this.conflict}); +} + +@ShouldThrow( + 'The JSON key `conflict` is conflicting with the discriminator ' + 'of sealed superclass `SuperWithConflictingDefaultCaseDiscriminator`', + todo: 'Rename the field or the discriminator.', + element: 'conflict', +) +@JsonSerializable() +class SubWithConflictingDiscriminatorWithDefaultNameImpl + extends SuperWithConflictingDefaultCaseDiscriminator { + final String conflict; + + SubWithConflictingDiscriminatorWithDefaultNameImpl({required this.conflict}); +} + +@JsonSerializable(unionDiscriminator: 'this_will_conflict') +sealed class SuperSuperSuperWithConflictingNestedDiscriminator {} + +@JsonSerializable(unionDiscriminator: 'this_is_fine') +sealed class SuperSuperWithConflictingNestedDiscriminator + extends SuperSuperSuperWithConflictingNestedDiscriminator {} + +@ShouldThrow( + 'The classes `SuperSuperSuperWithConflictingNestedDiscriminator` and ' + '`SuperWithConflictingNestedDiscriminator` are nested sealed classes, ' + 'but they have the same discriminator `this_will_conflict`.', + todo: + 'Rename one of the discriminators with `unionDiscriminator` ' + 'field of `@JsonSerializable`.', + element: 'SuperWithConflictingNestedDiscriminator', +) +@JsonSerializable(unionDiscriminator: 'this_will_conflict') +sealed class SuperWithConflictingNestedDiscriminator + extends SuperSuperWithConflictingNestedDiscriminator {} diff --git a/json_serializable/test/src/field_namer_input.dart b/json_serializable/test/src/field_namer_input.dart index 6541857e5..43188fe35 100644 --- a/json_serializable/test/src/field_namer_input.dart +++ b/json_serializable/test/src/field_namer_input.dart @@ -9,7 +9,7 @@ Map _$FieldNamerNoneToJson(FieldNamerNone instance) => 'NAME_OVERRIDE': instance.nameOverride, }; ''') -@JsonSerializable(fieldRename: FieldRename.none, createFactory: false) +@JsonSerializable(fieldRename: RenameType.none, createFactory: false) class FieldNamerNone { late String theField; @@ -24,7 +24,7 @@ Map _$FieldNamerKebabToJson(FieldNamerKebab instance) => 'NAME_OVERRIDE': instance.nameOverride, }; ''') -@JsonSerializable(fieldRename: FieldRename.kebab, createFactory: false) +@JsonSerializable(fieldRename: RenameType.kebab, createFactory: false) class FieldNamerKebab { late String theField; @@ -39,7 +39,7 @@ Map _$FieldNamerPascalToJson(FieldNamerPascal instance) => 'NAME_OVERRIDE': instance.nameOverride, }; ''') -@JsonSerializable(fieldRename: FieldRename.pascal, createFactory: false) +@JsonSerializable(fieldRename: RenameType.pascal, createFactory: false) class FieldNamerPascal { late String theField; @@ -54,7 +54,7 @@ Map _$FieldNamerSnakeToJson(FieldNamerSnake instance) => 'NAME_OVERRIDE': instance.nameOverride, }; ''') -@JsonSerializable(fieldRename: FieldRename.snake, createFactory: false) +@JsonSerializable(fieldRename: RenameType.snake, createFactory: false) class FieldNamerSnake { late String theField; @@ -70,7 +70,7 @@ Map _$FieldNamerScreamingSnakeToJson( 'nameOverride': instance.nameOverride, }; ''') -@JsonSerializable(fieldRename: FieldRename.screamingSnake, createFactory: false) +@JsonSerializable(fieldRename: RenameType.screamingSnake, createFactory: false) class FieldNamerScreamingSnake { late String theField; diff --git a/json_serializable/test/src/generic_test_input.dart b/json_serializable/test/src/generic_test_input.dart index 810c5a674..6d50ad441 100644 --- a/json_serializable/test/src/generic_test_input.dart +++ b/json_serializable/test/src/generic_test_input.dart @@ -85,3 +85,75 @@ Map _$GenericArgumentFactoriesFlagWithoutGenericTypeToJson( ) @JsonSerializable(genericArgumentFactories: true) class GenericArgumentFactoriesFlagWithoutGenericType {} + +@ShouldThrow( + 'The class `SuperWithGenericArgumentFactories` is annotated ' + 'with `JsonSerializable` field `genericArgumentFactories: true`. ' + '`genericArgumentFactories: true` is not supported for classes ' + 'that are sealed or have sealed superclasses.', + todo: + 'Remove the `genericArgumentFactories` option or ' + 'remove the `sealed` keyword from the class.', + element: 'SuperWithGenericArgumentFactories', +) +@JsonSerializable(genericArgumentFactories: true) +sealed class SuperWithGenericArgumentFactories {} + +@JsonSerializable(genericArgumentFactories: false) +sealed class SuperWithoutGenericArgumentFactories {} + +@ShouldThrow( + 'The class `SubWithSubGenericArgumentFactoriesExt` is annotated ' + 'with `JsonSerializable` field `genericArgumentFactories: true`. ' + '`genericArgumentFactories: true` is not supported for classes ' + 'that are sealed or have sealed superclasses.', + todo: + 'Remove the `genericArgumentFactories` option or ' + 'remove the `sealed` keyword from the class.', + element: 'SubWithSubGenericArgumentFactoriesExt', +) +@JsonSerializable(genericArgumentFactories: true) +class SubWithSubGenericArgumentFactoriesExt + extends SuperWithoutGenericArgumentFactories {} + +@ShouldThrow( + 'The class `SubWithSubGenericArgumentFactoriesImpl` is annotated ' + 'with `JsonSerializable` field `genericArgumentFactories: true`. ' + '`genericArgumentFactories: true` is not supported for classes ' + 'that are sealed or have sealed superclasses.', + todo: + 'Remove the `genericArgumentFactories` option or ' + 'remove the `sealed` keyword from the class.', + element: 'SubWithSubGenericArgumentFactoriesImpl', +) +@JsonSerializable(genericArgumentFactories: true) +class SubWithSubGenericArgumentFactoriesImpl + implements SuperWithoutGenericArgumentFactories {} + +@ShouldThrow( + 'The class `SubWithSubAndSuperGenericArgumentFactoriesExt` is annotated ' + 'with `JsonSerializable` field `genericArgumentFactories: true`. ' + '`genericArgumentFactories: true` is not supported for classes ' + 'that are sealed or have sealed superclasses.', + todo: + 'Remove the `genericArgumentFactories` option or ' + 'remove the `sealed` keyword from the class.', + element: 'SubWithSubAndSuperGenericArgumentFactoriesExt', +) +@JsonSerializable(genericArgumentFactories: true) +class SubWithSubAndSuperGenericArgumentFactoriesExt + extends SuperWithGenericArgumentFactories {} + +@ShouldThrow( + 'The class `SubWithSubAndSuperGenericArgumentFactoriesImpl` is annotated ' + 'with `JsonSerializable` field `genericArgumentFactories: true`. ' + '`genericArgumentFactories: true` is not supported for classes ' + 'that are sealed or have sealed superclasses.', + todo: + 'Remove the `genericArgumentFactories` option or ' + 'remove the `sealed` keyword from the class.', + element: 'SubWithSubAndSuperGenericArgumentFactoriesImpl', +) +@JsonSerializable(genericArgumentFactories: true) +class SubWithSubAndSuperGenericArgumentFactoriesImpl + implements SuperWithGenericArgumentFactories {} diff --git a/json_serializable/test/src/mismatching_config_input.dart b/json_serializable/test/src/mismatching_config_input.dart new file mode 100644 index 000000000..1c9e7598a --- /dev/null +++ b/json_serializable/test/src/mismatching_config_input.dart @@ -0,0 +1,45 @@ +// @dart=3.8 + +part of '_json_serializable_test_input.dart'; + +@JsonSerializable(createToJson: false) +sealed class SuperWithoutToJson {} + +@JsonSerializable(createToJson: true) +sealed class SuperWithToJson {} + +@ShouldThrow( + 'The class `SuperWithoutToJson` is sealed but its ' + 'subclass `SubWithToJsonAndSuperWithoutToJsonExt` has a different ' + '`createToJson` option than the base class.', + element: 'SubWithToJsonAndSuperWithoutToJsonExt', +) +@JsonSerializable(createToJson: true) +class SubWithToJsonAndSuperWithoutToJsonExt extends SuperWithoutToJson {} + +@ShouldThrow( + 'The class `SuperWithoutToJson` is sealed but its ' + 'subclass `SubWithToJsonAndSuperWithoutToJsonImpl` has a different ' + '`createToJson` option than the base class.', + element: 'SubWithToJsonAndSuperWithoutToJsonImpl', +) +@JsonSerializable(createToJson: true) +class SubWithToJsonAndSuperWithoutToJsonImpl implements SuperWithoutToJson {} + +@ShouldThrow( + 'The class `SuperWithToJson` is sealed but its ' + 'subclass `SubWithoutToJsonAndSuperWithToJsonExt` has a different ' + '`createToJson` option than the base class.', + element: 'SubWithoutToJsonAndSuperWithToJsonExt', +) +@JsonSerializable(createToJson: false) +class SubWithoutToJsonAndSuperWithToJsonExt extends SuperWithToJson {} + +@ShouldThrow( + 'The class `SuperWithToJson` is sealed but its ' + 'subclass `SubWithoutToJsonAndSuperWithToJsonImpl` has a different ' + '`createToJson` option than the base class.', + element: 'SubWithoutToJsonAndSuperWithToJsonImpl', +) +@JsonSerializable(createToJson: false) +class SubWithoutToJsonAndSuperWithToJsonImpl implements SuperWithToJson {} diff --git a/json_serializable/test/src/missing_annotation_input.dart b/json_serializable/test/src/missing_annotation_input.dart new file mode 100644 index 000000000..ad5cd3ec5 --- /dev/null +++ b/json_serializable/test/src/missing_annotation_input.dart @@ -0,0 +1,58 @@ +// @dart=3.8 + +part of '_json_serializable_test_input.dart'; + +sealed class SuperWithoutSuperJsonSerializableAnnotation {} + +@ShouldThrow( + 'The class `SubWithoutSuperJsonSerializableAnnotationExt` is annotated ' + 'with `JsonSerializable` but its sealed superclass ' + '`SuperWithoutSuperJsonSerializableAnnotation` is not annotated ' + 'with `JsonSerializable`.', + element: 'SubWithoutSuperJsonSerializableAnnotationExt', +) +@JsonSerializable() +class SubWithoutSuperJsonSerializableAnnotationExt + extends SuperWithoutSuperJsonSerializableAnnotation {} + +@ShouldThrow( + 'The class `SubWithoutSuperJsonSerializableAnnotationImpl` is annotated ' + 'with `JsonSerializable` but its sealed superclass ' + '`SuperWithoutSuperJsonSerializableAnnotation` is not annotated ' + 'with `JsonSerializable`.', + todo: 'Add `@JsonSerializable` annotation to the sealed class.', + element: 'SubWithoutSuperJsonSerializableAnnotationImpl', +) +@JsonSerializable() +class SubWithoutSuperJsonSerializableAnnotationImpl + implements SuperWithoutSuperJsonSerializableAnnotation {} + +@ShouldThrow( + 'The class `SuperWithSubExtWithoutJsonSerializableAnnotation` is sealed but ' + 'its subclass `SubWithoutJsonSerializableAnnotationExt` is not annotated ' + 'with `JsonSerializable`.', + todo: + 'Add `@JsonSerializable` annotation to ' + 'SubWithoutJsonSerializableAnnotationExt.', + element: 'SubWithoutJsonSerializableAnnotationExt', +) +@JsonSerializable() +sealed class SuperWithSubExtWithoutJsonSerializableAnnotation {} + +class SubWithoutJsonSerializableAnnotationExt + extends SuperWithSubExtWithoutJsonSerializableAnnotation {} + +@ShouldThrow( + 'The class `SuperWithSubImplWithoutJsonSerializableAnnotation` is sealed but ' + 'its subclass `SubWithoutJsonSerializableAnnotationImpl` is not annotated ' + 'with `JsonSerializable`.', + todo: + 'Add `@JsonSerializable` annotation to ' + 'SubWithoutJsonSerializableAnnotationImpl.', + element: 'SubWithoutJsonSerializableAnnotationImpl', +) +@JsonSerializable() +sealed class SuperWithSubImplWithoutJsonSerializableAnnotation {} + +class SubWithoutJsonSerializableAnnotationImpl + implements SuperWithSubImplWithoutJsonSerializableAnnotation {} diff --git a/json_serializable/test/src/sealed_test_input.dart b/json_serializable/test/src/sealed_test_input.dart new file mode 100644 index 000000000..882e7e786 --- /dev/null +++ b/json_serializable/test/src/sealed_test_input.dart @@ -0,0 +1,710 @@ +// @dart=3.8 + +part of '_json_serializable_test_input.dart'; + +@ShouldGenerate(r''' +SuperSimpleSealedClass _$SuperSimpleSealedClassFromJson( + Map json, +) => switch (json['type']) { + 'SubOneSimpleSealedClass' => _$SubOneSimpleSealedClassFromJson(json), + 'SubTwoSimpleSealedClass' => _$SubTwoSimpleSealedClassFromJson(json), + _ => throw UnrecognizedUnionTypeException( + '${json['type']}', + SuperSimpleSealedClass, + json, + ), +}; + +Map _$SuperSimpleSealedClassToJson( + SuperSimpleSealedClass instance, +) => switch (instance) { + final SubOneSimpleSealedClass instance => { + 'type': 'SubOneSimpleSealedClass', + ..._$SubOneSimpleSealedClassToJson(instance), + }, + final SubTwoSimpleSealedClass instance => { + 'type': 'SubTwoSimpleSealedClass', + ..._$SubTwoSimpleSealedClassToJson(instance), + }, +}; +''') +@JsonSerializable() +sealed class SuperSimpleSealedClass { + const SuperSimpleSealedClass(); +} + +@ShouldGenerate(r''' +SubOneSimpleSealedClass _$SubOneSimpleSealedClassFromJson( + Map json, +) => SubOneSimpleSealedClass(someField: json['someField'] as String); + +Map _$SubOneSimpleSealedClassToJson( + SubOneSimpleSealedClass instance, +) => {'someField': instance.someField}; +''') +@JsonSerializable() +class SubOneSimpleSealedClass extends SuperSimpleSealedClass { + final String someField; + + SubOneSimpleSealedClass({required this.someField}); +} + +@ShouldGenerate(r''' +SubTwoSimpleSealedClass _$SubTwoSimpleSealedClassFromJson( + Map json, +) => SubTwoSimpleSealedClass(someField: json['someField'] as String); + +Map _$SubTwoSimpleSealedClassToJson( + SubTwoSimpleSealedClass instance, +) => {'someField': instance.someField}; +''') +@JsonSerializable() +class SubTwoSimpleSealedClass extends SuperSimpleSealedClass { + final String someField; + + SubTwoSimpleSealedClass({required this.someField}); +} + +@ShouldGenerate(r''' +SuperSimpleSealedClassWithChangedDiscriminator +_$SuperSimpleSealedClassWithChangedDiscriminatorFromJson( + Map json, +) => switch (json['new_discriminator']) { + 'SubOneSimpleSealedClassWithChangedDiscriminator' => + _$SubOneSimpleSealedClassWithChangedDiscriminatorFromJson(json), + 'SubTwoSimpleSealedClassWithChangedDiscriminator' => + _$SubTwoSimpleSealedClassWithChangedDiscriminatorFromJson(json), + _ => throw UnrecognizedUnionTypeException( + '${json['new_discriminator']}', + SuperSimpleSealedClassWithChangedDiscriminator, + json, + ), +}; + +Map _$SuperSimpleSealedClassWithChangedDiscriminatorToJson( + SuperSimpleSealedClassWithChangedDiscriminator instance, +) => switch (instance) { + final SubOneSimpleSealedClassWithChangedDiscriminator instance => { + 'new_discriminator': 'SubOneSimpleSealedClassWithChangedDiscriminator', + ..._$SubOneSimpleSealedClassWithChangedDiscriminatorToJson(instance), + }, + final SubTwoSimpleSealedClassWithChangedDiscriminator instance => { + 'new_discriminator': 'SubTwoSimpleSealedClassWithChangedDiscriminator', + ..._$SubTwoSimpleSealedClassWithChangedDiscriminatorToJson(instance), + }, +}; +''') +@JsonSerializable(unionDiscriminator: 'new_discriminator') +sealed class SuperSimpleSealedClassWithChangedDiscriminator { + const SuperSimpleSealedClassWithChangedDiscriminator(); +} + +@ShouldGenerate(r''' +SubOneSimpleSealedClassWithChangedDiscriminator +_$SubOneSimpleSealedClassWithChangedDiscriminatorFromJson( + Map json, +) => SubOneSimpleSealedClassWithChangedDiscriminator( + someField: json['someField'] as String, +); + +Map _$SubOneSimpleSealedClassWithChangedDiscriminatorToJson( + SubOneSimpleSealedClassWithChangedDiscriminator instance, +) => {'someField': instance.someField}; +''') +@JsonSerializable() +class SubOneSimpleSealedClassWithChangedDiscriminator + extends SuperSimpleSealedClassWithChangedDiscriminator { + final String someField; + + SubOneSimpleSealedClassWithChangedDiscriminator({required this.someField}); +} + +@ShouldGenerate(r''' +SubTwoSimpleSealedClassWithChangedDiscriminator +_$SubTwoSimpleSealedClassWithChangedDiscriminatorFromJson( + Map json, +) => SubTwoSimpleSealedClassWithChangedDiscriminator( + someField: json['someField'] as String, +); + +Map _$SubTwoSimpleSealedClassWithChangedDiscriminatorToJson( + SubTwoSimpleSealedClassWithChangedDiscriminator instance, +) => {'someField': instance.someField}; +''') +@JsonSerializable() +class SubTwoSimpleSealedClassWithChangedDiscriminator + extends SuperSimpleSealedClassWithChangedDiscriminator { + final String someField; + + SubTwoSimpleSealedClassWithChangedDiscriminator({required this.someField}); +} + +@ShouldGenerate(r''' +SuperSimpleSealedClassWithChangedUnionRename +_$SuperSimpleSealedClassWithChangedUnionRenameFromJson( + Map json, +) => switch (json['type']) { + 'sub_one_simple_sealed_class_with_changed_union_rename' => + _$SubOneSimpleSealedClassWithChangedUnionRenameFromJson(json), + 'sub_two_simple_sealed_class_with_changed_union_rename' => + _$SubTwoSimpleSealedClassWithChangedUnionRenameFromJson(json), + _ => throw UnrecognizedUnionTypeException( + '${json['type']}', + SuperSimpleSealedClassWithChangedUnionRename, + json, + ), +}; + +Map _$SuperSimpleSealedClassWithChangedUnionRenameToJson( + SuperSimpleSealedClassWithChangedUnionRename instance, +) => switch (instance) { + final SubOneSimpleSealedClassWithChangedUnionRename instance => { + 'type': 'sub_one_simple_sealed_class_with_changed_union_rename', + ..._$SubOneSimpleSealedClassWithChangedUnionRenameToJson(instance), + }, + final SubTwoSimpleSealedClassWithChangedUnionRename instance => { + 'type': 'sub_two_simple_sealed_class_with_changed_union_rename', + ..._$SubTwoSimpleSealedClassWithChangedUnionRenameToJson(instance), + }, +}; +''') +@JsonSerializable(unionRename: RenameType.snake) +sealed class SuperSimpleSealedClassWithChangedUnionRename { + const SuperSimpleSealedClassWithChangedUnionRename(); +} + +@ShouldGenerate(r''' +SubOneSimpleSealedClassWithChangedUnionRename +_$SubOneSimpleSealedClassWithChangedUnionRenameFromJson( + Map json, +) => SubOneSimpleSealedClassWithChangedUnionRename( + someField: json['someField'] as String, +); + +Map _$SubOneSimpleSealedClassWithChangedUnionRenameToJson( + SubOneSimpleSealedClassWithChangedUnionRename instance, +) => {'someField': instance.someField}; +''') +@JsonSerializable() +class SubOneSimpleSealedClassWithChangedUnionRename + extends SuperSimpleSealedClassWithChangedUnionRename { + final String someField; + + SubOneSimpleSealedClassWithChangedUnionRename({required this.someField}); +} + +@ShouldGenerate(r''' +SubTwoSimpleSealedClassWithChangedUnionRename +_$SubTwoSimpleSealedClassWithChangedUnionRenameFromJson( + Map json, +) => SubTwoSimpleSealedClassWithChangedUnionRename( + someField: json['someField'] as String, +); + +Map _$SubTwoSimpleSealedClassWithChangedUnionRenameToJson( + SubTwoSimpleSealedClassWithChangedUnionRename instance, +) => {'someField': instance.someField}; +''') +@JsonSerializable() +class SubTwoSimpleSealedClassWithChangedUnionRename + extends SuperSimpleSealedClassWithChangedUnionRename { + final String someField; + + SubTwoSimpleSealedClassWithChangedUnionRename({required this.someField}); +} + +@ShouldGenerate(r''' +SuperSimpleSealedClassWithChangedDiscriminatorAndUnionRename +_$SuperSimpleSealedClassWithChangedDiscriminatorAndUnionRenameFromJson( + Map json, +) => switch (json['my_discriminator']) { + 'sub-one-simple-sealed-class-with-changed-discriminator-and-union-rename' => + _$SubOneSimpleSealedClassWithChangedDiscriminatorAndUnionRenameFromJson( + json, + ), + 'sub-two-simple-sealed-class-with-changed-discriminator-and-union-rename' => + _$SubTwoSimpleSealedClassWithChangedDiscriminatorAndUnionRenameFromJson( + json, + ), + _ => throw UnrecognizedUnionTypeException( + '${json['my_discriminator']}', + SuperSimpleSealedClassWithChangedDiscriminatorAndUnionRename, + json, + ), +}; + +Map +_$SuperSimpleSealedClassWithChangedDiscriminatorAndUnionRenameToJson( + SuperSimpleSealedClassWithChangedDiscriminatorAndUnionRename instance, +) => switch (instance) { + final SubOneSimpleSealedClassWithChangedDiscriminatorAndUnionRename + instance => + { + 'my_discriminator': + 'sub-one-simple-sealed-class-with-changed-discriminator-and-union-rename', + ..._$SubOneSimpleSealedClassWithChangedDiscriminatorAndUnionRenameToJson( + instance, + ), + }, + final SubTwoSimpleSealedClassWithChangedDiscriminatorAndUnionRename + instance => + { + 'my_discriminator': + 'sub-two-simple-sealed-class-with-changed-discriminator-and-union-rename', + ..._$SubTwoSimpleSealedClassWithChangedDiscriminatorAndUnionRenameToJson( + instance, + ), + }, +}; +''') +@JsonSerializable( + unionDiscriminator: 'my_discriminator', + unionRename: RenameType.kebab, +) +sealed class SuperSimpleSealedClassWithChangedDiscriminatorAndUnionRename { + const SuperSimpleSealedClassWithChangedDiscriminatorAndUnionRename(); +} + +@ShouldGenerate(r''' +SubOneSimpleSealedClassWithChangedDiscriminatorAndUnionRename +_$SubOneSimpleSealedClassWithChangedDiscriminatorAndUnionRenameFromJson( + Map json, +) => SubOneSimpleSealedClassWithChangedDiscriminatorAndUnionRename( + someField: json['someField'] as String, +); + +Map +_$SubOneSimpleSealedClassWithChangedDiscriminatorAndUnionRenameToJson( + SubOneSimpleSealedClassWithChangedDiscriminatorAndUnionRename instance, +) => {'someField': instance.someField}; +''') +@JsonSerializable() +class SubOneSimpleSealedClassWithChangedDiscriminatorAndUnionRename + extends SuperSimpleSealedClassWithChangedDiscriminatorAndUnionRename { + final String someField; + + SubOneSimpleSealedClassWithChangedDiscriminatorAndUnionRename({ + required this.someField, + }); +} + +@ShouldGenerate(r''' +SubTwoSimpleSealedClassWithChangedDiscriminatorAndUnionRename +_$SubTwoSimpleSealedClassWithChangedDiscriminatorAndUnionRenameFromJson( + Map json, +) => SubTwoSimpleSealedClassWithChangedDiscriminatorAndUnionRename( + someField: json['someField'] as String, +); + +Map +_$SubTwoSimpleSealedClassWithChangedDiscriminatorAndUnionRenameToJson( + SubTwoSimpleSealedClassWithChangedDiscriminatorAndUnionRename instance, +) => {'someField': instance.someField}; +''') +@JsonSerializable() +class SubTwoSimpleSealedClassWithChangedDiscriminatorAndUnionRename + extends SuperSimpleSealedClassWithChangedDiscriminatorAndUnionRename { + final String someField; + + SubTwoSimpleSealedClassWithChangedDiscriminatorAndUnionRename({ + required this.someField, + }); +} + +@ShouldGenerate(r''' +SuperSimpleSealedClassWithoutToJson +_$SuperSimpleSealedClassWithoutToJsonFromJson(Map json) => + switch (json['type']) { + _ => throw UnrecognizedUnionTypeException( + '${json['type']}', + SuperSimpleSealedClassWithoutToJson, + json, + ), + }; +''') +@JsonSerializable(createToJson: false) +sealed class SuperSimpleSealedClassWithoutToJson {} + +@ShouldGenerate(r''' +SubSimpleSealedClassWithoutToJson _$SubSimpleSealedClassWithoutToJsonFromJson( + Map json, +) => SubSimpleSealedClassWithoutToJson(someField: json['someField'] as String); +''') +@JsonSerializable(createToJson: false) +class SubSimpleSealedClassWithoutToJson { + final String someField; + + SubSimpleSealedClassWithoutToJson({required this.someField}); +} + +@ShouldGenerate(r''' +SuperSuperSuperNested _$SuperSuperSuperNestedFromJson( + Map json, +) => switch (json['super_super_super_type']) { + 'SuperSuperNestedOne' => _$SuperSuperNestedOneFromJson(json), + 'SuperSuperNestedTwo' => _$SuperSuperNestedTwoFromJson(json), + _ => throw UnrecognizedUnionTypeException( + '${json['super_super_super_type']}', + SuperSuperSuperNested, + json, + ), +}; + +Map _$SuperSuperSuperNestedToJson( + SuperSuperSuperNested instance, +) => switch (instance) { + final SuperSuperNestedOne instance => { + 'super_super_super_type': 'SuperSuperNestedOne', + ..._$SuperSuperNestedOneToJson(instance), + }, + final SuperSuperNestedTwo instance => { + 'super_super_super_type': 'SuperSuperNestedTwo', + ..._$SuperSuperNestedTwoToJson(instance), + }, +}; +''') +@JsonSerializable(unionDiscriminator: 'super_super_super_type') +sealed class SuperSuperSuperNested { + const SuperSuperSuperNested(); +} + +@ShouldGenerate(r''' +SuperSuperNestedOne _$SuperSuperNestedOneFromJson(Map json) => + switch (json['super_super_type']) { + 'SuperNestedOneOne' => _$SuperNestedOneOneFromJson(json), + 'SuperNestedOneTwo' => _$SuperNestedOneTwoFromJson(json), + _ => throw UnrecognizedUnionTypeException( + '${json['super_super_type']}', + SuperSuperNestedOne, + json, + ), + }; + +Map _$SuperSuperNestedOneToJson( + SuperSuperNestedOne instance, +) => switch (instance) { + final SuperNestedOneOne instance => { + 'super_super_type': 'SuperNestedOneOne', + ..._$SuperNestedOneOneToJson(instance), + }, + final SuperNestedOneTwo instance => { + 'super_super_type': 'SuperNestedOneTwo', + ..._$SuperNestedOneTwoToJson(instance), + }, +}; +''') +@JsonSerializable(unionDiscriminator: 'super_super_type') +sealed class SuperSuperNestedOne extends SuperSuperSuperNested { + const SuperSuperNestedOne(); +} + +@ShouldGenerate(r''' +SuperSuperNestedTwo _$SuperSuperNestedTwoFromJson(Map json) => + switch (json['super_super_type']) { + 'SuperNestedTwoOne' => _$SuperNestedTwoOneFromJson(json), + 'SuperNestedTwoTwo' => _$SuperNestedTwoTwoFromJson(json), + _ => throw UnrecognizedUnionTypeException( + '${json['super_super_type']}', + SuperSuperNestedTwo, + json, + ), + }; + +Map _$SuperSuperNestedTwoToJson( + SuperSuperNestedTwo instance, +) => switch (instance) { + final SuperNestedTwoOne instance => { + 'super_super_type': 'SuperNestedTwoOne', + ..._$SuperNestedTwoOneToJson(instance), + }, + final SuperNestedTwoTwo instance => { + 'super_super_type': 'SuperNestedTwoTwo', + ..._$SuperNestedTwoTwoToJson(instance), + }, +}; +''') +@JsonSerializable(unionDiscriminator: 'super_super_type') +sealed class SuperSuperNestedTwo extends SuperSuperSuperNested { + const SuperSuperNestedTwo(); +} + +@ShouldGenerate(r''' +SuperNestedOneOne _$SuperNestedOneOneFromJson(Map json) => + switch (json['super_type']) { + 'SubNestedOneOne' => _$SubNestedOneOneFromJson(json), + _ => throw UnrecognizedUnionTypeException( + '${json['super_type']}', + SuperNestedOneOne, + json, + ), + }; + +Map _$SuperNestedOneOneToJson(SuperNestedOneOne instance) => + switch (instance) { + final SubNestedOneOne instance => { + 'super_type': 'SubNestedOneOne', + ..._$SubNestedOneOneToJson(instance), + }, + }; +''') +@JsonSerializable(unionDiscriminator: 'super_type') +sealed class SuperNestedOneOne extends SuperSuperNestedOne { + const SuperNestedOneOne(); +} + +@ShouldGenerate(r''' +SuperNestedOneTwo _$SuperNestedOneTwoFromJson(Map json) => + switch (json['super_type']) { + 'SubNestedOneTwo' => _$SubNestedOneTwoFromJson(json), + _ => throw UnrecognizedUnionTypeException( + '${json['super_type']}', + SuperNestedOneTwo, + json, + ), + }; + +Map _$SuperNestedOneTwoToJson(SuperNestedOneTwo instance) => + switch (instance) { + final SubNestedOneTwo instance => { + 'super_type': 'SubNestedOneTwo', + ..._$SubNestedOneTwoToJson(instance), + }, + }; +''') +@JsonSerializable(unionDiscriminator: 'super_type') +sealed class SuperNestedOneTwo extends SuperSuperNestedOne { + const SuperNestedOneTwo(); +} + +@ShouldGenerate(r''' +SuperNestedTwoOne _$SuperNestedTwoOneFromJson(Map json) => + switch (json['super_type']) { + 'SubNestedTwoOne' => _$SubNestedTwoOneFromJson(json), + _ => throw UnrecognizedUnionTypeException( + '${json['super_type']}', + SuperNestedTwoOne, + json, + ), + }; + +Map _$SuperNestedTwoOneToJson(SuperNestedTwoOne instance) => + switch (instance) { + final SubNestedTwoOne instance => { + 'super_type': 'SubNestedTwoOne', + ..._$SubNestedTwoOneToJson(instance), + }, + }; +''') +@JsonSerializable(unionDiscriminator: 'super_type') +sealed class SuperNestedTwoOne extends SuperSuperNestedTwo { + const SuperNestedTwoOne(); +} + +@ShouldGenerate(r''' +SuperNestedTwoTwo _$SuperNestedTwoTwoFromJson(Map json) => + switch (json['super_type']) { + 'SubNestedTwoTwo' => _$SubNestedTwoTwoFromJson(json), + _ => throw UnrecognizedUnionTypeException( + '${json['super_type']}', + SuperNestedTwoTwo, + json, + ), + }; + +Map _$SuperNestedTwoTwoToJson(SuperNestedTwoTwo instance) => + switch (instance) { + final SubNestedTwoTwo instance => { + 'super_type': 'SubNestedTwoTwo', + ..._$SubNestedTwoTwoToJson(instance), + }, + }; +''') +@JsonSerializable(unionDiscriminator: 'super_type') +sealed class SuperNestedTwoTwo extends SuperSuperNestedTwo { + const SuperNestedTwoTwo(); +} + +@ShouldGenerate(r''' +SubNestedOneOne _$SubNestedOneOneFromJson(Map json) => + SubNestedOneOne(oneOneField: json['oneOneField'] as String); + +Map _$SubNestedOneOneToJson(SubNestedOneOne instance) => + {'oneOneField': instance.oneOneField}; +''') +@JsonSerializable() +class SubNestedOneOne extends SuperNestedOneOne { + final String oneOneField; + + SubNestedOneOne({required this.oneOneField}); +} + +@ShouldGenerate(r''' +SubNestedOneTwo _$SubNestedOneTwoFromJson(Map json) => + SubNestedOneTwo(oneTwoField: json['oneTwoField'] as String); + +Map _$SubNestedOneTwoToJson(SubNestedOneTwo instance) => + {'oneTwoField': instance.oneTwoField}; +''') +@JsonSerializable() +class SubNestedOneTwo extends SuperNestedOneTwo { + final String oneTwoField; + + SubNestedOneTwo({required this.oneTwoField}); +} + +@ShouldGenerate(r''' +SubNestedTwoOne _$SubNestedTwoOneFromJson(Map json) => + SubNestedTwoOne(twoOneField: json['twoOneField'] as String); + +Map _$SubNestedTwoOneToJson(SubNestedTwoOne instance) => + {'twoOneField': instance.twoOneField}; +''') +@JsonSerializable() +class SubNestedTwoOne extends SuperNestedTwoOne { + final String twoOneField; + + SubNestedTwoOne({required this.twoOneField}); +} + +@ShouldGenerate(r''' +SubNestedTwoTwo _$SubNestedTwoTwoFromJson(Map json) => + SubNestedTwoTwo(twoTwoField: json['twoTwoField'] as String); + +Map _$SubNestedTwoTwoToJson(SubNestedTwoTwo instance) => + {'twoTwoField': instance.twoTwoField}; +''') +@JsonSerializable() +class SubNestedTwoTwo extends SuperNestedTwoTwo { + final String twoTwoField; + + SubNestedTwoTwo({required this.twoTwoField}); +} + +@ShouldGenerate(r''' +SuperMultipleImplOne _$SuperMultipleImplOneFromJson( + Map json, +) => switch (json['type']) { + 'SubOneMultipleImpl' => _$SubOneMultipleImplFromJson(json), + 'SubThreeMultipleImpl' => _$SubThreeMultipleImplFromJson(json), + 'SubFourMultipleImpl' => _$SubFourMultipleImplFromJson(json), + _ => throw UnrecognizedUnionTypeException( + '${json['type']}', + SuperMultipleImplOne, + json, + ), +}; + +Map _$SuperMultipleImplOneToJson( + SuperMultipleImplOne instance, +) => switch (instance) { + final SubOneMultipleImpl instance => { + 'type': 'SubOneMultipleImpl', + ..._$SubOneMultipleImplToJson(instance), + }, + final SubThreeMultipleImpl instance => { + 'type': 'SubThreeMultipleImpl', + ..._$SubThreeMultipleImplToJson(instance), + }, + final SubFourMultipleImpl instance => { + 'type': 'SubFourMultipleImpl', + ..._$SubFourMultipleImplToJson(instance), + }, +}; +''') +@JsonSerializable() +sealed class SuperMultipleImplOne {} + +@ShouldGenerate(r''' +SuperMultipleImplTwo _$SuperMultipleImplTwoFromJson( + Map json, +) => switch (json['type']) { + 'SubTwoMultipleImpl' => _$SubTwoMultipleImplFromJson(json), + 'SubThreeMultipleImpl' => _$SubThreeMultipleImplFromJson(json), + 'SubFourMultipleImpl' => _$SubFourMultipleImplFromJson(json), + _ => throw UnrecognizedUnionTypeException( + '${json['type']}', + SuperMultipleImplTwo, + json, + ), +}; + +Map _$SuperMultipleImplTwoToJson( + SuperMultipleImplTwo instance, +) => switch (instance) { + final SubTwoMultipleImpl instance => { + 'type': 'SubTwoMultipleImpl', + ..._$SubTwoMultipleImplToJson(instance), + }, + final SubThreeMultipleImpl instance => { + 'type': 'SubThreeMultipleImpl', + ..._$SubThreeMultipleImplToJson(instance), + }, + final SubFourMultipleImpl instance => { + 'type': 'SubFourMultipleImpl', + ..._$SubFourMultipleImplToJson(instance), + }, +}; +''') +@JsonSerializable() +sealed class SuperMultipleImplTwo {} + +@ShouldGenerate(r''' +SubOneMultipleImpl _$SubOneMultipleImplFromJson(Map json) => + SubOneMultipleImpl(json['subOneField'] as String); + +Map _$SubOneMultipleImplToJson(SubOneMultipleImpl instance) => + {'subOneField': instance.subOneField}; +''') +@JsonSerializable() +class SubOneMultipleImpl implements SuperMultipleImplOne { + final String subOneField; + + SubOneMultipleImpl(this.subOneField); +} + +@ShouldGenerate(r''' +SubTwoMultipleImpl _$SubTwoMultipleImplFromJson(Map json) => + SubTwoMultipleImpl(json['subTwoField'] as String); + +Map _$SubTwoMultipleImplToJson(SubTwoMultipleImpl instance) => + {'subTwoField': instance.subTwoField}; +''') +@JsonSerializable() +class SubTwoMultipleImpl implements SuperMultipleImplTwo { + final String subTwoField; + + SubTwoMultipleImpl(this.subTwoField); +} + +@ShouldGenerate(r''' +SubThreeMultipleImpl _$SubThreeMultipleImplFromJson( + Map json, +) => SubThreeMultipleImpl(json['subThreeField'] as String); + +Map _$SubThreeMultipleImplToJson( + SubThreeMultipleImpl instance, +) => {'subThreeField': instance.subThreeField}; +''') +@JsonSerializable() +class SubThreeMultipleImpl + implements SuperMultipleImplOne, SuperMultipleImplTwo { + final String subThreeField; + + SubThreeMultipleImpl(this.subThreeField); +} + +@ShouldGenerate(r''' +SubFourMultipleImpl _$SubFourMultipleImplFromJson(Map json) => + SubFourMultipleImpl(json['subFourField'] as String); + +Map _$SubFourMultipleImplToJson( + SubFourMultipleImpl instance, +) => {'subFourField': instance.subFourField}; +''') +@JsonSerializable() +class SubFourMultipleImpl + implements SuperMultipleImplOne, SuperMultipleImplTwo { + final String subFourField; + + SubFourMultipleImpl(this.subFourField); +} diff --git a/json_serializable/test/src/union_namer_input.dart b/json_serializable/test/src/union_namer_input.dart new file mode 100644 index 000000000..80081c001 --- /dev/null +++ b/json_serializable/test/src/union_namer_input.dart @@ -0,0 +1,181 @@ +// @dart=3.8 + +part of '_json_serializable_test_input.dart'; + +@ShouldGenerate(r''' +SuperUnionRenameNone _$SuperUnionRenameNoneFromJson( + Map json, +) => switch (json['type']) { + 'SubUnionRenameNone' => _$SubUnionRenameNoneFromJson(json), + _ => throw UnrecognizedUnionTypeException( + '${json['type']}', + SuperUnionRenameNone, + json, + ), +}; + +Map _$SuperUnionRenameNoneToJson( + SuperUnionRenameNone instance, +) => switch (instance) { + final SubUnionRenameNone instance => { + 'type': 'SubUnionRenameNone', + ..._$SubUnionRenameNoneToJson(instance), + }, +}; +''') +@JsonSerializable(unionRename: RenameType.none) +sealed class SuperUnionRenameNone {} + +@ShouldGenerate(r''' +SubUnionRenameNone _$SubUnionRenameNoneFromJson(Map json) => + SubUnionRenameNone(); + +Map _$SubUnionRenameNoneToJson(SubUnionRenameNone instance) => + {}; +''') +@JsonSerializable() +class SubUnionRenameNone extends SuperUnionRenameNone {} + +@ShouldGenerate(r''' +SuperUnionRenameKebab _$SuperUnionRenameKebabFromJson( + Map json, +) => switch (json['type']) { + 'sub-union-rename-kebab' => _$SubUnionRenameKebabFromJson(json), + _ => throw UnrecognizedUnionTypeException( + '${json['type']}', + SuperUnionRenameKebab, + json, + ), +}; + +Map _$SuperUnionRenameKebabToJson( + SuperUnionRenameKebab instance, +) => switch (instance) { + final SubUnionRenameKebab instance => { + 'type': 'sub-union-rename-kebab', + ..._$SubUnionRenameKebabToJson(instance), + }, +}; +''') +@JsonSerializable(unionRename: RenameType.kebab) +sealed class SuperUnionRenameKebab {} + +@ShouldGenerate(r''' +SubUnionRenameKebab _$SubUnionRenameKebabFromJson(Map json) => + SubUnionRenameKebab(); + +Map _$SubUnionRenameKebabToJson( + SubUnionRenameKebab instance, +) => {}; +''') +@JsonSerializable() +class SubUnionRenameKebab extends SuperUnionRenameKebab {} + +@ShouldGenerate(r''' +SuperUnionRenameSnake _$SuperUnionRenameSnakeFromJson( + Map json, +) => switch (json['type']) { + 'sub_union_rename_snake' => _$SubUnionRenameSnakeFromJson(json), + _ => throw UnrecognizedUnionTypeException( + '${json['type']}', + SuperUnionRenameSnake, + json, + ), +}; + +Map _$SuperUnionRenameSnakeToJson( + SuperUnionRenameSnake instance, +) => switch (instance) { + final SubUnionRenameSnake instance => { + 'type': 'sub_union_rename_snake', + ..._$SubUnionRenameSnakeToJson(instance), + }, +}; +''') +@JsonSerializable(unionRename: RenameType.snake) +sealed class SuperUnionRenameSnake {} + +@ShouldGenerate(r''' +SubUnionRenameSnake _$SubUnionRenameSnakeFromJson(Map json) => + SubUnionRenameSnake(); + +Map _$SubUnionRenameSnakeToJson( + SubUnionRenameSnake instance, +) => {}; +''') +@JsonSerializable() +class SubUnionRenameSnake extends SuperUnionRenameSnake {} + +@ShouldGenerate(r''' +SuperUnionRenamePascal _$SuperUnionRenamePascalFromJson( + Map json, +) => switch (json['type']) { + 'SubUnionRenamePascal' => _$SubUnionRenamePascalFromJson(json), + _ => throw UnrecognizedUnionTypeException( + '${json['type']}', + SuperUnionRenamePascal, + json, + ), +}; + +Map _$SuperUnionRenamePascalToJson( + SuperUnionRenamePascal instance, +) => switch (instance) { + final SubUnionRenamePascal instance => { + 'type': 'SubUnionRenamePascal', + ..._$SubUnionRenamePascalToJson(instance), + }, +}; +''') +@JsonSerializable(unionRename: RenameType.pascal) +sealed class SuperUnionRenamePascal {} + +@ShouldGenerate(r''' +SubUnionRenamePascal _$SubUnionRenamePascalFromJson( + Map json, +) => SubUnionRenamePascal(); + +Map _$SubUnionRenamePascalToJson( + SubUnionRenamePascal instance, +) => {}; +''') +@JsonSerializable() +class SubUnionRenamePascal extends SuperUnionRenamePascal {} + +@ShouldGenerate(r''' +SuperUnionRenameScreamingSnake _$SuperUnionRenameScreamingSnakeFromJson( + Map json, +) => switch (json['type']) { + 'SUB_UNION_RENAME_SCREAMING_SNAKE' => _$SubUnionRenameScreamingSnakeFromJson( + json, + ), + _ => throw UnrecognizedUnionTypeException( + '${json['type']}', + SuperUnionRenameScreamingSnake, + json, + ), +}; + +Map _$SuperUnionRenameScreamingSnakeToJson( + SuperUnionRenameScreamingSnake instance, +) => switch (instance) { + final SubUnionRenameScreamingSnake instance => { + 'type': 'SUB_UNION_RENAME_SCREAMING_SNAKE', + ..._$SubUnionRenameScreamingSnakeToJson(instance), + }, +}; +''') +@JsonSerializable(unionRename: RenameType.screamingSnake) +sealed class SuperUnionRenameScreamingSnake {} + +@ShouldGenerate(r''' +SubUnionRenameScreamingSnake _$SubUnionRenameScreamingSnakeFromJson( + Map json, +) => SubUnionRenameScreamingSnake(); + +Map _$SubUnionRenameScreamingSnakeToJson( + SubUnionRenameScreamingSnake instance, +) => {}; +''') +@JsonSerializable() +class SubUnionRenameScreamingSnake extends SuperUnionRenameScreamingSnake {} diff --git a/json_serializable/test/test_sources/test_sources.dart b/json_serializable/test/test_sources/test_sources.dart index 80a08aa3b..e25fc8104 100644 --- a/json_serializable/test/test_sources/test_sources.dart +++ b/json_serializable/test/test_sources/test_sources.dart @@ -20,10 +20,12 @@ class ConfigurationImplicitDefaults { createPerFieldToJson: false, disallowUnrecognizedKeys: false, explicitToJson: false, - fieldRename: FieldRename.none, + fieldRename: RenameType.none, ignoreUnannotated: false, includeIfNull: true, genericArgumentFactories: false, + unionDiscriminator: 'type', + unionRename: RenameType.none, ) class ConfigurationExplicitDefaults { int? field; diff --git a/json_serializable/tool/readme/readme_template.md b/json_serializable/tool/readme/readme_template.md index d99c3ec78..c377a4ac1 100644 --- a/json_serializable/tool/readme/readme_template.md +++ b/json_serializable/tool/readme/readme_template.md @@ -64,7 +64,7 @@ precedence over any value set on `ja:JsonSerializable`. Annotate `enum` types with `ja:JsonEnum` (new in `json_annotation` 4.2.0) to: 1. Specify the default rename logic for each enum value using `fieldRename`. For - instance, use `fieldRename: FieldRename.kebab` to encode `enum` value + instance, use `fieldRename: RenameType.kebab` to encode `enum` value `noGood` as `"no-good"`. 1. Force the generation of the `enum` helpers, even if the `enum` is not referenced in code. This is an edge scenario, but useful for some. @@ -121,6 +121,21 @@ customize the encoding/decoding of any type, you have a few options. +# Sealed classes + +As of `json_serializable` version 6.10.0 and `json_annotation` +version 4.10.0, sealed classes can be serialized to json unions and json unions +can be deserialized to sealed classes. + +To achieve this, both the sealed class and its subclasses should be annotated +with `ja:JsonSerializable`. Only the sealed class should have `fromJson` factory +or `toJson` function. To customize the sealed class behavior, use the fields +`unionRename` and `unionDiscriminator` in `ja:JsonSerializable` or adjust the +default behavior by changing the corresponding fields in `build.yaml`. For +more complex examples, please see [example]: + + + # Build configuration Aside from setting arguments on the associated annotation classes, you can also @@ -150,6 +165,8 @@ targets: generic_argument_factories: false ignore_unannotated: false include_if_null: true + union_discriminator: type + union_rename: none ``` To exclude generated files from coverage, you can further configure `build.yaml`. diff --git a/json_serializable/tool/readme_builder.dart b/json_serializable/tool/readme_builder.dart index 3709ee017..4807d625e 100644 --- a/json_serializable/tool/readme_builder.dart +++ b/json_serializable/tool/readme_builder.dart @@ -26,6 +26,7 @@ class _ReadmeBuilder extends Builder { ...await buildStep.getExampleContent('example/example.dart'), ...await buildStep.getExampleContent('example/example.g.dart'), ...await buildStep.getExampleContent('tool/readme/readme_examples.dart'), + ...await buildStep.getExampleContent('example/sealed_example.dart'), 'supported_types': _classCleanAndSort(supportedTypes()), 'collection_types': _classCleanAndSort(collectionTypes()), 'map_key_types': _classCleanAndSort(mapKeyTypes),