Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

JSON serialization emitDefaults option #592

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
1 change: 1 addition & 0 deletions protobuf/lib/protobuf.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import 'dart:typed_data' show TypedData, Uint8List, ByteData, Endian;
import 'package:fixnum/fixnum.dart' show Int64;

import 'src/protobuf/json_parsing_context.dart';
import 'src/protobuf/json_serialization_context.dart';
import 'src/protobuf/permissive_compare.dart';
import 'src/protobuf/type_registry.dart';
export 'src/protobuf/type_registry.dart' show TypeRegistry;
Expand Down
5 changes: 3 additions & 2 deletions protobuf/lib/src/protobuf/generated_message.dart
Original file line number Diff line number Diff line change
Expand Up @@ -233,8 +233,9 @@ abstract class GeneratedMessage {
/// message encoding a type not in [typeRegistry] is encountered, an
/// error is thrown.
Object? toProto3Json(
{TypeRegistry typeRegistry = const TypeRegistry.empty()}) =>
_writeToProto3Json(_fieldSet, typeRegistry);
{TypeRegistry typeRegistry = const TypeRegistry.empty(),
bool emitDefaults = false}) =>
_writeToProto3Json(_fieldSet, typeRegistry, emitDefaults);

/// Merges field values from [json], a JSON object using proto3 encoding.
///
Expand Down
9 changes: 9 additions & 0 deletions protobuf/lib/src/protobuf/json_serialization_context.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright (c) 2022, 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.

class JsonSerializationContext {
final bool emitDefaults;

JsonSerializationContext(this.emitDefaults);
}
42 changes: 37 additions & 5 deletions protobuf/lib/src/protobuf/proto3_json.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@

part of protobuf;

Object? _writeToProto3Json(_FieldSet fs, TypeRegistry typeRegistry) {
Object? _writeToProto3Json(
_FieldSet fs, TypeRegistry typeRegistry, bool emitDefaults) {
var context = JsonSerializationContext(emitDefaults);

String? convertToMapKey(dynamic key, int keyType) {
var baseType = PbFieldType._baseType(keyType);

Expand Down Expand Up @@ -36,8 +39,8 @@ Object? _writeToProto3Json(_FieldSet fs, TypeRegistry typeRegistry) {
if (fieldValue == null) return null;

if (_isGroupOrMessage(fieldType!)) {
return _writeToProto3Json(
(fieldValue as GeneratedMessage)._fieldSet, typeRegistry);
return _writeToProto3Json((fieldValue as GeneratedMessage)._fieldSet,
typeRegistry, context.emitDefaults);
} else if (_isEnum(fieldType)) {
return (fieldValue as ProtobufEnum).name;
} else {
Expand Down Expand Up @@ -81,6 +84,14 @@ Object? _writeToProto3Json(_FieldSet fs, TypeRegistry typeRegistry) {
}
}

bool isNullOrEmptyList(dynamic value) {
return value == null || (value is List && value.isEmpty);
}

bool isNullOrEmptyMap(dynamic value) {
return value == null || (value is Map && value.isEmpty);
}

final meta = fs._meta;
if (meta.toProto3Json != null) {
return meta.toProto3Json!(fs._message!, typeRegistry);
Expand All @@ -89,11 +100,32 @@ Object? _writeToProto3Json(_FieldSet fs, TypeRegistry typeRegistry) {
var result = <String, dynamic>{};
for (var fieldInfo in fs._infosSortedByTag) {
var value = fs._values[fieldInfo.index!];
if (value == null || (value is List && value.isEmpty)) {
var overrideForEmitsDefaults = false;
jmartin127 marked this conversation as resolved.
Show resolved Hide resolved
dynamic overrideForEmitsDefaultsValue;
if (context.emitDefaults) {
if (fieldInfo.isRepeated && isNullOrEmptyList(value)) {
overrideForEmitsDefaults = true;
overrideForEmitsDefaultsValue = [];
} else if (fieldInfo.isMapField && isNullOrEmptyMap(value)) {
overrideForEmitsDefaults = true;
overrideForEmitsDefaultsValue = {};
} else if (_isBytes(fieldInfo.type) && isNullOrEmptyList(value)) {
jmartin127 marked this conversation as resolved.
Show resolved Hide resolved
overrideForEmitsDefaults = true;
overrideForEmitsDefaultsValue = null;
} else if (_isGroupOrMessage(fieldInfo.type) && value == null) {
jmartin127 marked this conversation as resolved.
Show resolved Hide resolved
overrideForEmitsDefaults = true;
overrideForEmitsDefaultsValue = null;
} else {
value ??= fieldInfo.makeDefault!();
}
jmartin127 marked this conversation as resolved.
Show resolved Hide resolved
}
if (isNullOrEmptyList(value) && !overrideForEmitsDefaults) {
continue; // It's missing, repeated, or an empty byte array.
}
dynamic jsonValue;
if (fieldInfo.isMapField) {
if (overrideForEmitsDefaults) {
jsonValue = overrideForEmitsDefaultsValue;
} else if (fieldInfo.isMapField) {
jsonValue = (value as PbMap).map((key, entryValue) {
var mapEntryInfo = fieldInfo as MapFieldInfo;
return MapEntry(convertToMapKey(key, mapEntryInfo.keyFieldType!),
Expand Down
58 changes: 58 additions & 0 deletions protobuf/test/json_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,64 @@ void main() {
final decoded = T()..mergeFromJsonMap(encoded);
expect(decoded.int64, value);
});

test('testToProto3Json', () {
var json = jsonEncode(example.toProto3Json());
checkProto3JsonMap(jsonDecode(json), 3);
});

test('testToProto3JsonEmitDefaults', () {
var json = jsonEncode(example.toProto3Json(emitDefaults: true));
checkProto3JsonMap(jsonDecode(json), 6);
expect(json.contains('"child":null'), isTrue);
});

test('testToProto3JsonEmitDefaultsNoValues', () {
final exampleAllDefaults = T();
var json = jsonEncode(exampleAllDefaults.toProto3Json(emitDefaults: true));
Map m = jsonDecode(json);
expect(m.length, 6);
});

test('testToProto3JsonEmitDefaultsWithChild', () {
var child = example;

var parent = T()
..val = 123
..str = 'hello'
..int32s.addAll(<int>[1, 2, 3])
..child = example;
var parentJson = jsonEncode(parent.toProto3Json(emitDefaults: true));
var childJson = jsonEncode(child.toProto3Json(emitDefaults: true));
checkProto3JsonMap(jsonDecode(parentJson), 6);
expect(parentJson.contains(childJson), isTrue);
});

test('testToProto3JsonEmitDefaultsWithNullList', () {
var exampleEmptyList = T()
..val = example.val
..str = example.str;

var json = jsonEncode(exampleEmptyList.toProto3Json(emitDefaults: true));
expect(json.contains('"int32s":[]'), isTrue);
});

test('testToProto3JsonEmitDefaultsWithEmptyList', () {
var exampleEmptyList = T()
..val = example.val
..str = example.str
..int32s.addAll(<int>[]);

var json = jsonEncode(exampleEmptyList.toProto3Json(emitDefaults: true));
expect(json.contains('"int32s":[]'), isTrue);
});
}

void checkProto3JsonMap(Map m, int expectedLength) {
expect(m.length, expectedLength);
expect(m['val'], 123);
expect(m['str'], 'hello');
expect(m['int32s'], [1, 2, 3]);
}

void checkJsonMap(Map m) {
Expand Down