Initial extraction of the json macro into a package, without configuration.

Change-Id: I94067a57151066736e3c703ef85bfa90a4af1c70
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/365081
Reviewed-by: Bob Nystrom <rnystrom@google.com>
Reviewed-by: Marya Belanger <mbelanger@google.com>
Reviewed-by: Morgan :) <davidmorgan@google.com>
Commit-Queue: Jake Macdonald <jakemac@google.com>
This commit is contained in:
Jake Macdonald 2024-05-02 17:20:29 +00:00 committed by Commit Queue
parent f5fe3dddcc
commit 2f40aab365
7 changed files with 837 additions and 0 deletions

3
pkg/json/CHANGELOG.md Normal file
View file

@ -0,0 +1,3 @@
# 0.1.0-wip
- Initial release, adds the JsonCodable macro.

27
pkg/json/LICENSE Normal file
View file

@ -0,0 +1,27 @@
Copyright 2024, the Dart project authors.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of Google LLC nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

93
pkg/json/README.md Normal file
View file

@ -0,0 +1,93 @@
This package provides an experimental macro that encodes and decodes
user-defined Dart classes to JSON maps (maps of type `Map<String, Object?>`).
This relies on the experimental macros language feature, and as such may be
unstable or have breaking changes if the feature changes.
## Applying the JsonCodable macro
To apply the JsonCodable macro to a class, add it as an annotation:
```dart
import 'package:json/json.dart';
@JsonCodable()
class User {
final String name;
final int? age;
}
```
The macro generates two members for the `User` class: a `fromJson` constructor
and a `toJson` method, with APIs like the following:
```dart
class User {
User.fromJson(Map<String, Object?> json);
Map<String, Object?> toJson();
}
```
Each non-nullable field in the annotated class must have an entry with the same name in the `json` map, but
nullable fields are allowed to have no key at all. The `toJson` will omit null
fields entirely from the map, and will not contain explicit null entries.
### Extending other classes
You are allowed to extend classes other than `Object`, but they must have a
valid `toJson` method and `fromJson` constructor, so the following is allowed:
```dart
@JsonCodable()
class Manager extends User {
final List<User> reports;
}
```
## Supported field types
All native JSON types are supported (`int`, `double`, `String`, `bool`, `Null`).
The core collection types `List`, `Set`, and `Map` are also supported, if their
elements are supported types. For elements which require more than just a cast,
the type must be statically provided through a generic type argument on the
collection in order for the specialized code to be generated.
Additionally, custom types with a `toJson` method and `fromJson` constructor are
allowed, including as generic type arguments to the collection types.
## Generics
Classes with generic type parameters are not supported, but may be in the
future.
## Configuration
Macro configuration is a feature we intend to add, but it is not available at this time.
Because of this, field names must exactly match the keys in the maps, and
default values are not supported.
## Enabling the macros experiment
Most tools accept the `--enable-experiment=macros` option, and appending that
to your typical command line invocations should be all that is needed. For
example, you can launch your flutter project like:
`flutter run --enable-experiment=macros`.
For the analyzer, you will want to add some configuration to an
`analysis_options.yaml` file at the root of your project:
```yaml
analyzer:
enable-experiment:
- macros
```
Note that `dart run` is a little bit special, in that the option must come
_immediately_ following `dart` and before `run` - this is because it is an
option to the Dart VM, and not the Dart script itself. For example,
`dart --enable-experiment=macros run bin/my_script.dart`. This is also how the
`test` package expects to be invoked, so `dart --enable-experiment=macros test`.

View file

@ -0,0 +1,5 @@
include: package:lints/recommended.yaml
analyzer:
enable-experiment:
- macros

639
pkg/json/lib/json.dart Normal file
View file

@ -0,0 +1,639 @@
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// ignore_for_file: deprecated_member_use
import 'dart:async';
import 'package:macros/macros.dart';
/// A macro which adds a `fromJson(Map<String, Object?> json)` constructor and
/// a `Map<String, Object?> toJson()` method to a class.
///
/// To use this macro, annotate your class with `@JsonCodable()` and enable the
/// macros experiment (see README.md for full instructions).
///
/// The implementations are derived from the fields defined directly on the
/// annotated class, and field names are expected to exactly match the keys of
/// the maps that they are being decoded from.
///
/// If extending any class other than [Object], then the super class is expected
/// to also have a corresponding `toJson` method and `fromJson` constructor.
///
/// Annotated classes are not allowed to have a manually defined `toJson` method
/// or `fromJson` constructor.
macro class JsonCodable
implements ClassDeclarationsMacro, ClassDefinitionMacro {
const JsonCodable();
/// Declares the `fromJson` constructor and `toJson` method, but does not
/// implement them.
@override
Future<void> buildDeclarationsForClass(
ClassDeclaration clazz, MemberDeclarationBuilder builder) async {
if (clazz.typeParameters.isNotEmpty) {
throw DiagnosticException(Diagnostic(DiagnosticMessage(
// TODO: Target the actual type parameter, issue #55611
'Cannot be applied to classes with generic type parameters'),
Severity.error));
}
final (map, string, object, _, _) = await (
builder.resolveIdentifier(_dartCore, 'Map'),
builder.resolveIdentifier(_dartCore, 'String'),
builder.resolveIdentifier(_dartCore, 'Object'),
// These are just for validation, and will throw if the check fails.
_checkNoFromJson(builder, clazz),
_checkNoToJson(builder, clazz),
).wait;
final mapStringObject = NamedTypeAnnotationCode(name: map, typeArguments: [
NamedTypeAnnotationCode(name: string),
NamedTypeAnnotationCode(name: object).asNullable
]);
builder.declareInType(DeclarationCode.fromParts([
// TODO(language#3580): Remove/replace 'external'?
' external ',
clazz.identifier.name,
'.fromJson(',
mapStringObject,
' json);',
]));
builder.declareInType(DeclarationCode.fromParts([
// TODO(language#3580): Remove/replace 'external'?
' external ',
mapStringObject,
' toJson();',
]));
}
/// Provides the actual definitions of the `fromJson` constructor and `toJson`
/// method, which were declared in the previous phase.
@override
Future<void> buildDefinitionForClass(
ClassDeclaration clazz, TypeDefinitionBuilder builder) async {
final (introspectionData, constructors, methods) = await (
_SharedIntrospectionData.build(builder, clazz),
builder.constructorsOf(clazz),
builder.methodsOf(clazz)
).wait;
final fromJson =
constructors.firstWhereOrNull((c) => c.identifier.name == 'fromJson');
final toJson =
methods.firstWhereOrNull((c) => c.identifier.name == 'toJson');
// An earlier step failed, just bail out without emitting additional
// diagnostics.
if (fromJson == null || toJson == null) return;
final (fromJsonBuilder, toJsonBuilder) = await (
builder.buildConstructor(fromJson.identifier),
builder.buildMethod(toJson.identifier),
).wait;
await (
_buildFromJson(fromJson, fromJsonBuilder, introspectionData),
_buildToJson(toJson, toJsonBuilder, introspectionData),
).wait;
}
/// Builds the actual `fromJson` constructor.
Future<void> _buildFromJson(
ConstructorDeclaration constructor,
ConstructorDefinitionBuilder builder,
_SharedIntrospectionData introspectionData) async {
await _checkValidFromJson(constructor, introspectionData, builder);
// If extending something other than `Object`, it must have a `fromJson`
// constructor.
var superclassHasFromJson = false;
final superclassDeclaration = introspectionData.superclass;
if (superclassDeclaration != null &&
!superclassDeclaration.isExactly('Object', _dartCore)) {
final superclassConstructors =
await builder.constructorsOf(superclassDeclaration);
for (final superConstructor in superclassConstructors) {
if (superConstructor.identifier.name == 'fromJson') {
await _checkValidFromJson(
superConstructor, introspectionData, builder);
superclassHasFromJson = true;
break;
}
}
if (!superclassHasFromJson) {
throw DiagnosticException(Diagnostic(
DiagnosticMessage(
'Serialization of classes that extend other classes is only '
'supported if those classes have a valid '
'`fromJson(Map<String, Object?> json)` constructor.',
target: introspectionData.clazz.superclass?.asDiagnosticTarget),
Severity.error));
}
}
final fields = introspectionData.fields;
final jsonParam = constructor.positionalParameters.single.identifier;
Future<Code> initializerForField(FieldDeclaration field) async {
return RawCode.fromParts([
field.identifier,
' = ',
await _convertTypeFromJson(
field.type,
RawCode.fromParts([
jsonParam,
"['",
field.identifier.name,
"']",
]),
builder,
introspectionData),
]);
}
final initializers = await Future.wait(fields.map(initializerForField));
if (superclassHasFromJson) {
initializers.add(RawCode.fromParts([
'super.fromJson(',
jsonParam,
')',
]));
}
builder.augment(initializers: initializers);
}
/// Builds the actual `toJson` method.
Future<void> _buildToJson(
MethodDeclaration method,
FunctionDefinitionBuilder builder,
_SharedIntrospectionData introspectionData) async {
if (!(await _checkValidToJson(method, introspectionData, builder))) return;
// If extending something other than `Object`, it must have a `toJson`
// method.
var superclassHasToJson = false;
final superclassDeclaration = introspectionData.superclass;
if (superclassDeclaration != null &&
!superclassDeclaration.isExactly('Object', _dartCore)) {
final superclassMethods = await builder.methodsOf(superclassDeclaration);
for (final superMethod in superclassMethods) {
if (superMethod.identifier.name == 'toJson') {
if (!(await _checkValidToJson(
superMethod, introspectionData, builder))) {
return;
}
superclassHasToJson = true;
break;
}
}
if (!superclassHasToJson) {
builder.report(Diagnostic(
DiagnosticMessage(
'Serialization of classes that extend other classes is only '
'supported if those classes have a valid '
'`Map<String, Object?> toJson()` method.',
target: introspectionData.clazz.superclass?.asDiagnosticTarget),
Severity.error));
return;
}
}
final fields = introspectionData.fields;
final parts = <Object>[
'{\n final json = ',
if (superclassHasToJson)
'super.toJson()'
else ...[
'<',
introspectionData.stringCode,
', ',
introspectionData.objectCode.asNullable,
'>{}',
],
';\n ',
];
Future<Code> addEntryForField(FieldDeclaration field) async {
final parts = <Object>[];
final doNullCheck = field.type.isNullable;
if (doNullCheck) {
parts.addAll([
'if (',
field.identifier,
// `null` is a reserved word, we can just use it.
' != null) {\n ',
]);
}
parts.addAll([
"json['",
field.identifier.name,
"'] = ",
await _convertTypeToJson(
field.type,
RawCode.fromParts([
field.identifier,
if (doNullCheck) '!',
]),
builder,
introspectionData),
';\n',
]);
if (doNullCheck) {
parts.add(' }\n');
}
return RawCode.fromParts(parts);
}
parts.addAll(await Future.wait(fields.map(addEntryForField)));
parts.add(' return json;\n }');
builder.augment(FunctionBodyCode.fromParts(parts));
}
/// Throws a [DiagnosticException] if there is an existing `fromJson`
/// constructor on [clazz].
Future<void> _checkNoFromJson(
DeclarationBuilder builder, ClassDeclaration clazz) async {
final constructors = await builder.constructorsOf(clazz);
final fromJson =
constructors.firstWhereOrNull((c) => c.identifier.name == 'fromJson');
if (fromJson != null) {
throw DiagnosticException(Diagnostic(
DiagnosticMessage(
'Cannot generate a fromJson constructor due to this existing '
'one.',
target: fromJson.asDiagnosticTarget),
Severity.error));
}
}
/// Throws a [DiagnosticException] if there is an existing `toJson` method on
/// [clazz].
Future<void> _checkNoToJson(
DeclarationBuilder builder, ClassDeclaration clazz) async {
final methods = await builder.methodsOf(clazz);
final toJson =
methods.firstWhereOrNull((m) => m.identifier.name == 'toJson');
if (toJson != null) {
throw DiagnosticException(Diagnostic(
DiagnosticMessage(
'Cannot generate a toJson method due to this existing one.',
target: toJson.asDiagnosticTarget),
Severity.error));
}
}
/// Checks that [constructor] is a valid `fromJson` constructor, and throws a
/// [DiagnosticException] if not.
Future<void> _checkValidFromJson(
ConstructorDeclaration constructor,
_SharedIntrospectionData introspectionData,
DefinitionBuilder builder) async {
if (constructor.namedParameters.isNotEmpty ||
constructor.positionalParameters.length != 1 ||
!(await (await builder
.resolve(constructor.positionalParameters.single.type.code))
.isExactly(introspectionData.jsonMapType))) {
throw DiagnosticException(Diagnostic(
DiagnosticMessage(
'Expected exactly one parameter, with the type '
'Map<String, Object?>',
target: constructor.asDiagnosticTarget),
Severity.error));
}
}
/// Returns a [Code] object which is an expression that converts a JSON map
/// (referenced by [jsonReference]) into an instance of type [type].
Future<Code> _convertTypeFromJson(
TypeAnnotation type,
Code jsonReference,
DefinitionBuilder builder,
_SharedIntrospectionData introspectionData) async {
if (type is! NamedTypeAnnotation) {
builder.report(Diagnostic(
DiagnosticMessage(
'Only fields with named types are allowed on serializable '
'classes',
target: type.asDiagnosticTarget),
Severity.error));
return RawCode.fromString(
"throw 'Unable to deserialize type ${type.code.debugString}'");
}
// Follow type aliases until we reach an actual named type.
var classDecl = await _classDeclarationOf(builder, type);
if (classDecl == null) {
return RawCode.fromString(
"throw 'Unable to deserialize type ${type.code.debugString}'");
}
var nullCheck = type.isNullable
? RawCode.fromParts([
jsonReference,
// `null` is a reserved word, we can just use it.
' == null ? null : ',
])
: null;
// Check if `typeDecl` is one of the supported collection types.
if (classDecl.isExactly('List', _dartCore)) {
return RawCode.fromParts([
if (nullCheck != null) nullCheck,
'[ for (final item in ',
jsonReference,
' as ',
introspectionData.jsonListCode,
') ',
await _convertTypeFromJson(type.typeArguments.single,
RawCode.fromString('item'), builder, introspectionData),
']',
]);
} else if (classDecl.isExactly('Set', _dartCore)) {
return RawCode.fromParts([
if (nullCheck != null) nullCheck,
'{ for (final item in ',
jsonReference,
' as ',
introspectionData.jsonListCode,
')',
await _convertTypeFromJson(type.typeArguments.single,
RawCode.fromString('item'), builder, introspectionData),
'}',
]);
} else if (classDecl.isExactly('Map', _dartCore)) {
return RawCode.fromParts([
if (nullCheck != null) nullCheck,
'{ for (final entry in ',
jsonReference,
' as ',
introspectionData.jsonMapCode,
'.entries) entry.key: ',
await _convertTypeFromJson(type.typeArguments.single,
RawCode.fromString('entry.value'), builder, introspectionData),
'}',
]);
}
// Otherwise, check if `classDecl` has a `fromJson` constructor.
final constructors = await builder.constructorsOf(classDecl);
final fromJson = constructors
.firstWhereOrNull((c) => c.identifier.name == 'fromJson')
?.identifier;
if (fromJson != null) {
return RawCode.fromParts([
if (nullCheck != null) nullCheck,
fromJson,
'(',
jsonReference,
' as ',
introspectionData.jsonMapCode,
')',
]);
}
// Finally, we just cast directly to the field type.
// TODO: Check that it is a valid type we can cast from JSON.
return RawCode.fromParts([
jsonReference,
' as ',
type.code,
]);
}
/// Checks that [method] is a valid `toJson` method, and throws a
/// [DiagnosticException] if not.
Future<bool> _checkValidToJson(
MethodDeclaration method,
_SharedIntrospectionData introspectionData,
DefinitionBuilder builder) async {
if (method.namedParameters.isNotEmpty ||
method.positionalParameters.isNotEmpty ||
!(await (await builder.resolve(method.returnType.code))
.isExactly(introspectionData.jsonMapType))) {
builder.report(Diagnostic(
DiagnosticMessage(
'Expected no parameters, and a return type of '
'Map<String, Object?>',
target: method.asDiagnosticTarget),
Severity.error));
return false;
}
return true;
}
/// Returns a [Code] object which is an expression that converts an instance
/// of type [type] (referenced by [valueReference]) into a JSON map.
Future<Code> _convertTypeToJson(
TypeAnnotation type,
Code valueReference,
DefinitionBuilder builder,
_SharedIntrospectionData introspectionData) async {
if (type is! NamedTypeAnnotation) {
builder.report(Diagnostic(
DiagnosticMessage(
'Only fields with named types are allowed on serializable '
'classes',
target: type.asDiagnosticTarget),
Severity.error));
return RawCode.fromString(
"throw 'Unable to serialize type ${type.code.debugString}'");
}
// Follow type aliases until we reach an actual named type.
var classDecl = await _classDeclarationOf(builder, type);
if (classDecl == null) {
return RawCode.fromString(
"throw 'Unable to serialize type ${type.code.debugString}'");
}
// Check for the supported collection types, and serialize them accordingly.
if (classDecl.isExactly('List', _dartCore) ||
classDecl.isExactly('Set', _dartCore)) {
return RawCode.fromParts([
'[ for (final item in ',
valueReference,
') ',
await _convertTypeToJson(type.typeArguments.single,
RawCode.fromString('item'), builder, introspectionData),
']',
]);
} else if (classDecl.isExactly('Map', _dartCore)) {
return RawCode.fromParts([
'{ for (final entry in ',
valueReference,
'.entries) entry.key: ',
await _convertTypeToJson(type.typeArguments.single,
RawCode.fromString('entry.value'), builder, introspectionData),
'}',
]);
}
// Next, check if it has a `toJson()` method and call that.
final methods = await builder.methodsOf(classDecl);
final toJson = methods
.firstWhereOrNull((c) => c.identifier.name == 'toJson')
?.identifier;
if (toJson != null) {
return RawCode.fromParts([
valueReference,
'.toJson()',
]);
}
// Finally, we just return the value as is if we can't otherwise handle it.
// TODO: Check that it is a valid type we can serialize.
return valueReference;
}
/// Follows [type] through any type aliases, until it reaches a
/// [ClassDeclaration], or returns null if it does not bottom out on a class.
Future<ClassDeclaration?> _classDeclarationOf(
DefinitionBuilder builder, NamedTypeAnnotation type) async {
var typeDecl = await builder.typeDeclarationOf(type.identifier);
while (typeDecl is TypeAliasDeclaration) {
final aliasedType = typeDecl.aliasedType;
if (aliasedType is! NamedTypeAnnotation) {
builder.report(Diagnostic(
DiagnosticMessage(
'Only fields with named types are allowed on serializable '
'classes',
target: type.asDiagnosticTarget),
Severity.error));
return null;
}
typeDecl = await builder.typeDeclarationOf(aliasedType.identifier);
}
if (typeDecl is! ClassDeclaration) {
builder.report(Diagnostic(
DiagnosticMessage(
'Only classes are supported as field types for serializable '
'classes',
target: type.asDiagnosticTarget),
Severity.error));
return null;
}
return typeDecl;
}
}
/// This data is collected asynchronously, so we only want to do it once and
/// share that work across multiple locations.
final class _SharedIntrospectionData {
/// The declaration of the class we are generating for.
final ClassDeclaration clazz;
/// All the fields on the [clazz].
final List<FieldDeclaration> fields;
/// A [Code] representation of the type [List<Object?>].
final NamedTypeAnnotationCode jsonListCode;
/// A [Code] representation of the type [Map<String, Object?>].
final NamedTypeAnnotationCode jsonMapCode;
/// The resolved [StaticType] representing the [Map<String, Object?>] type.
final StaticType jsonMapType;
/// A [Code] representation of the type [Object].
final NamedTypeAnnotationCode objectCode;
/// A [Code] representation of the type [String].
final NamedTypeAnnotationCode stringCode;
/// The declaration of the superclass of [clazz], if it is not [Object].
final ClassDeclaration? superclass;
_SharedIntrospectionData({
required this.clazz,
required this.fields,
required this.jsonListCode,
required this.jsonMapCode,
required this.jsonMapType,
required this.objectCode,
required this.stringCode,
required this.superclass,
});
static Future<_SharedIntrospectionData> build(
DeclarationPhaseIntrospector builder, ClassDeclaration clazz) async {
final (list, map, object, string) = await (
builder.resolveIdentifier(_dartCore, 'List'),
builder.resolveIdentifier(_dartCore, 'Map'),
builder.resolveIdentifier(_dartCore, 'Object'),
builder.resolveIdentifier(_dartCore, 'String'),
).wait;
final objectCode = NamedTypeAnnotationCode(name: object);
final nullableObjectCode = objectCode.asNullable;
final jsonListCode = NamedTypeAnnotationCode(name: list, typeArguments: [
nullableObjectCode,
]);
final jsonMapCode = NamedTypeAnnotationCode(name: map, typeArguments: [
NamedTypeAnnotationCode(name: string),
nullableObjectCode,
]);
final stringCode = NamedTypeAnnotationCode(name: string);
final superclass = clazz.superclass;
final (fields, jsonMapType, superclassDecl) = await (
builder.fieldsOf(clazz),
builder.resolve(jsonMapCode),
superclass == null
? Future.value(null)
: builder.typeDeclarationOf(superclass.identifier),
).wait;
return _SharedIntrospectionData(
clazz: clazz,
fields: fields,
jsonListCode: jsonListCode,
jsonMapCode: jsonMapCode,
jsonMapType: jsonMapType,
objectCode: objectCode,
stringCode: stringCode,
superclass: superclassDecl as ClassDeclaration?,
);
}
}
final _dartCore = Uri.parse('dart:core');
extension _FirstWhereOrNull<T> on Iterable<T> {
T? firstWhereOrNull(bool Function(T) compare) {
for (final item in this) {
if (compare(item)) return item;
}
return null;
}
}
extension _IsExactly on TypeDeclaration {
/// Cheaper than checking types using a [StaticType].
bool isExactly(String name, Uri library) =>
identifier.name == name && this.library.uri == library;
}
extension on Code {
/// Used for error messages.
String get debugString {
final buffer = StringBuffer();
_writeDebugString(buffer);
return buffer.toString();
}
void _writeDebugString(StringBuffer buffer) {
for (final part in parts) {
switch (part) {
case Code():
part._writeDebugString(buffer);
case Identifier():
buffer.write(part.name);
default:
buffer.write(part);
}
}
}
}

13
pkg/json/pubspec.yaml Normal file
View file

@ -0,0 +1,13 @@
name: json
description: >
A package which provides an experimental macro that encodes and decodes
user-defined Dart classes to JSON maps (maps of type `Map<String, Object?>`).
repository: https://github.com/dart-lang/sdk/tree/main/pkg/json
version: 0.1.0-wip
environment:
sdk: ^3.4.0-0
dependencies:
macros: ^0.1.0-0
dev_dependencies:
lints: any
test: any

View file

@ -0,0 +1,57 @@
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
//
// SharedOptions=--enable-experiment=macros
import 'package:json/json.dart';
import 'package:test/test.dart';
void main() {
group('Can be encoded and decoded', () {
test('without nullable fields', () {
var userJson = {
'name': 'John',
'age': 35,
};
var user = User.fromJson(userJson);
expect(user.name, 'John');
expect(user.age, 35);
expect(user.friends, null);
expect(userJson, equals(user.toJson()));
});
test('with nullable fields', () {
var userJson = {
'name': 'John',
'age': 35,
'friends': [
{
'name': 'Jill',
'age': 28,
},
],
};
var user = User.fromJson(userJson);
expect(user.name, 'John');
expect(user.age, 35);
expect(user.friends?.length, 1);
var friend = user.friends!.single;
expect(friend.name, 'Jill');
expect(friend.age, 28);
expect(userJson, equals(user.toJson()));
});
});
}
@JsonCodable()
class User {
String name;
int age;
List<User>? friends;
}