Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions json_serializable/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
- Support `JsonKey.explicitJsonNullWhenNonNullField` for PATCH-style tri-state
JSON fields: distinguish omitted keys from explicit `null` in `fromJson` and
emit explicit JSON `null` in `toJson` when the Dart field is non-null.
- Fix `createJsonSchema` to emit an `enum` constraint (and `default`) for
enum-typed fields, respecting `@JsonValue` and `JsonEnum.valueField`.
([#1577](https://github.com/google/json_serializable.dart/issues/1577))
- Require `json_annotation: '>=4.12.0 <4.13.0'`

## 6.13.2
Expand Down
10 changes: 10 additions & 0 deletions json_serializable/lib/src/enum_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ String? enumValueMapFromType(
return 'const ${constMapName(targetType)} = {\n$items\n};';
}

/// If [targetType] is an enum, returns the values used to encode its constants
/// in JSON, in declaration order.
///
/// Otherwise, `null`.
List<Object?>? enumEncodedValues(DartType targetType) {
final enumMap = _enumMap(targetType);
if (enumMap == null) return null;
return enumMap.values.toList(growable: false);
}

Map<FieldElement, Object?>? _enumMap(
DartType targetType, {
bool nullWithNoAnnotation = false,
Expand Down
13 changes: 13 additions & 0 deletions json_serializable/lib/src/schema_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:analyzer/dart/element/type.dart';
import 'package:source_gen/source_gen.dart';
import 'package:source_helper/source_helper.dart';

import 'enum_utils.dart';
import 'field_helpers.dart';
import 'helper_core.dart';
import 'json_key_utils.dart';
Expand Down Expand Up @@ -239,6 +240,11 @@ Map<String, dynamic> _getPropertySchema(
};
}

final enumValues = enumEncodedValues(type);
if (enumValues != null) {
return {'enum': enumValues};
}

if (type is InterfaceType && !type.isDartCoreObject) {
return _generateComplexTypeSchema(
type,
Expand Down Expand Up @@ -320,9 +326,16 @@ Object? _defaultValue(DartObject defaultValue, DartType type) => switch (type) {
defaultValue.toListValue(),
_ when coreMapTypeChecker.isAssignableFromType(type) =>
defaultValue.toMapValue(),
_ when type.isEnum => _enumDefaultValue(defaultValue, type),
_ => _noMatch,
};

Object? _enumDefaultValue(DartObject defaultValue, DartType type) {
final values = enumEncodedValues(type);
if (values == null) return _noMatch;
return enumValueForDartObject<Object?>(defaultValue, values, (v) => '$v');
}

/// Sentinel value used to indicate that no default value could be determined.
final _noMatch = Object();

Expand Down
18 changes: 18 additions & 0 deletions json_serializable/test/integration/schema_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,21 @@ final class ComprehensiveNested {

static const schema = _$ComprehensiveNestedJsonSchema;
}

@JsonEnum()
enum Season { spring, summer, autumn, winter }

@JsonSerializable(createJsonSchema: true)
final class SchemaEnumExample {
final Season season;
final List<Season> seasons;

SchemaEnumExample(this.season, this.seasons);

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

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

static const schema = _$SchemaEnumExampleJsonSchema;
}
38 changes: 38 additions & 0 deletions json_serializable/test/integration/schema_example.g.dart

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

39 changes: 39 additions & 0 deletions json_serializable/test/integration/schema_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,43 @@ void main() {
);
});
});

group('SchemaEnumExample', () {
late JsonSchema schema;

setUpAll(() {
schema = JsonSchema.create(SchemaEnumExample.schema);
});

test('valid instance', () {
final instance = SchemaEnumExample(Season.summer, [
Season.spring,
Season.winter,
]);

final json = jsonDecode(jsonEncode(instance));
final validation = schema.validate(json);
expect(validation.isValid, isTrue, reason: validation.errors.toString());
});

test('invalid instance - value not in enum', () {
final json = <String, dynamic>{
'season': 'fall', // not a valid Season
'seasons': <dynamic>['spring'],
};

final validation = schema.validate(json);
expect(validation.isValid, isFalse);
});

test('invalid instance - bad value in enum list', () {
final json = <String, dynamic>{
'season': 'summer',
'seasons': <dynamic>['spring', 'fall'],
};

final validation = schema.validate(json);
expect(validation.isValid, isFalse);
});
});
}
1 change: 1 addition & 0 deletions json_serializable/test/json_serializable_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -196,4 +196,5 @@ const _expectedSchemaTests = {
'JsonSchemaGetterTest',
'JsonSchemaRecursiveListTest',
'JsonSchemaRecursiveListIssue',
'JsonSchemaEnumTest',
};
77 changes: 77 additions & 0 deletions json_serializable/test/src/_json_schema_test_input.dart
Original file line number Diff line number Diff line change
Expand Up @@ -290,3 +290,80 @@ class JsonSchemaRecursiveListIssueA {

JsonSchemaRecursiveListIssueA(this.children);
}

enum JsonSchemaCategory { fruit, vegetable, grain }

enum JsonSchemaPriority {
@JsonValue(1)
low,
@JsonValue(2)
medium,
@JsonValue(3)
high,
}

enum JsonSchemaNullableValue {
@JsonValue(null)
unknown,
@JsonValue('known')
known,
}

@ShouldGenerate(r'''
const _$JsonSchemaEnumTestJsonSchema = {
r'$schema': 'https://json-schema.org/draft/2020-12/schema',
'type': 'object',
'properties': {
'category': {
'enum': ['fruit', 'vegetable', 'grain'],
},
'categories': {
'type': 'array',
'items': {
'enum': ['fruit', 'vegetable', 'grain'],
},
},
'priority': {
'enum': [1, 2, 3],
'description': 'Encoded with `@JsonValue` overrides',
},
'nullableValue': {
'enum': [null, 'known'],
'description': '`@JsonValue(null)` is a valid member',
},
'priorityWithDefault': {
'enum': [1, 2, 3],
'description': 'Default uses the encoded value, respecting `@JsonValue`',
'default': 2,
},
},
'required': ['category', 'categories', 'priority', 'nullableValue'],
};
''')
@JsonSerializable(
createJsonSchema: true,
createFactory: false,
createToJson: false,
)
class JsonSchemaEnumTest {
final JsonSchemaCategory category;
final List<JsonSchemaCategory> categories;

/// Encoded with `@JsonValue` overrides
final JsonSchemaPriority priority;

/// `@JsonValue(null)` is a valid member
final JsonSchemaNullableValue nullableValue;

/// Default uses the encoded value, respecting `@JsonValue`
@JsonKey(defaultValue: JsonSchemaPriority.medium)
final JsonSchemaPriority priorityWithDefault;

JsonSchemaEnumTest(
this.category,
this.categories,
this.priority,
this.nullableValue,
this.priorityWithDefault,
);
}