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
18 changes: 18 additions & 0 deletions protobuf/lib/src/protobuf/json_serialization_context.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// 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);
}

class JsonSerializationException implements Exception {
final String message;

JsonSerializationException(this.message);

@override
String toString() => message;
}
108 changes: 88 additions & 20 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 @@ -88,25 +91,90 @@ 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)) {
continue; // It's missing, repeated, or an empty byte array.
}
dynamic jsonValue;
if (fieldInfo.isMapField) {
jsonValue = (value as PbMap).map((key, entryValue) {
var mapEntryInfo = fieldInfo as MapFieldInfo;
return MapEntry(convertToMapKey(key, mapEntryInfo.keyFieldType!),
valueToProto3Json(entryValue, mapEntryInfo.valueFieldType));
});
} else if (fieldInfo.isRepeated) {
jsonValue = (value as PbListBase)
.map((element) => valueToProto3Json(element, fieldInfo.type))
.toList();
// if the value for this field is null, this function will return the
// default value, which simplifies the need to handle null for most cases
// below
var value = fs._getField(fieldInfo.tagNumber);

bool skipField = true;
Object? jsonValue;
if (fieldInfo.isRepeated) {
if (context.emitDefaults || (value as List).isNotEmpty) {
skipField = false;
jsonValue = (value as PbListBase)
.map((element) => valueToProto3Json(element, fieldInfo.type))
.toList();
}
jmartin127 marked this conversation as resolved.
Show resolved Hide resolved
} else if (fieldInfo.isMapField) {
if (context.emitDefaults || (value as Map).isNotEmpty) {
Copy link
Member

Choose a reason for hiding this comment

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

Note to self: shouldn't default value for maps be null? I think maps are considered the same as messages in the spec, at least when talking about default values.

Copy link
Author

Choose a reason for hiding this comment

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

Great question. I know that the emitDefaults option within the Go library serializes maps that are not set to an empty map value: {}. The spec is not very clear on this point.

Let me know if we are thinking this should be changed.

Copy link
Member

Choose a reason for hiding this comment

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

This should be related to #309. I say not a blocker for this PR.

skipField = false;
jsonValue = (value as PbMap).map((key, entryValue) {
var mapEntryInfo = fieldInfo as MapFieldInfo;
return MapEntry(convertToMapKey(key, mapEntryInfo.keyFieldType!),
valueToProto3Json(entryValue, mapEntryInfo.valueFieldType));
});
}
} else if (_isBytes(fieldInfo.type)) {
if (context.emitDefaults || (value as List).isNotEmpty) {
skipField = false;
if ((value as List).isEmpty) {
jsonValue = null;
} else {
jsonValue = valueToProto3Json(value, fieldInfo.type);
}
}
} else if (fieldInfo.isEnum) {
// For enums, the default value is the first value listed in the enum's type definition
final defaultEnum = fieldInfo.enumValues!.first;
if (context.emitDefaults ||
(value as ProtobufEnum).name != defaultEnum.name) {
skipField = false;
jsonValue = valueToProto3Json(value, fieldInfo.type);
}
} else if (fieldInfo.isGroupOrMessage) {
final originalValue = fs._values[fieldInfo.index!];
if (context.emitDefaults || originalValue != null) {
skipField = false;
if (originalValue == null) {
jsonValue = null;
} else {
jsonValue = valueToProto3Json(value, fieldInfo.type);
}
}
} else if (PbFieldType._baseType(fieldInfo.type) ==
PbFieldType._STRING_BIT) {
if (context.emitDefaults || (value as String).isNotEmpty) {
skipField = false;
jsonValue = valueToProto3Json(value, fieldInfo.type);
}
} else if (PbFieldType._baseType(fieldInfo.type) == PbFieldType._BOOL_BIT) {
if (context.emitDefaults || value != false) {
skipField = false;
jsonValue = valueToProto3Json(value, fieldInfo.type);
}
} else if (value is Int64) {
if (context.emitDefaults || !value.isZero) {
skipField = false;
jsonValue = valueToProto3Json(value, fieldInfo.type);
}
} else if (value is int) {
if (context.emitDefaults || value != 0) {
skipField = false;
jsonValue = valueToProto3Json(value, fieldInfo.type);
}
} else if (value is double) {
if (context.emitDefaults || value != 0.0) {
skipField = false;
jsonValue = valueToProto3Json(value, fieldInfo.type);
}
Copy link
Member

Choose a reason for hiding this comment

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

Thanks @jmartin127 , this is not much easier to follow now.

I still have one suggestion though. In the outer if, in the first few cases you check the field type, which makes sense. But after the bool case you start checking the value type rather than field type. Ideally we should be able to check just the field type, and the value type should be as expected for the field type (i.e. value is int if field type is int32). I have a suggestion in my top-level comment on how to make this easier.

Copy link
Author

Choose a reason for hiding this comment

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

Yes, this part was a bit wonky. The reason I switched from value types to field types was because there are just so many numeric types to handle, so it became more readable to check the value types. I think if we follow you top-level comment suggestion, then this is no longer an issue. I'll wait for your and @sigurdm to reply on the best path forward there (if we go with the switch, from your other PR).

} else {
jsonValue = valueToProto3Json(value, fieldInfo.type);
throw JsonSerializationException(
'Unexpected field type for proto3 JSON serialization ${fieldInfo.type}');
}

if (!skipField) {
result[fieldInfo.name] = jsonValue;
}
result[fieldInfo.name] = jsonValue;
}
Comment on lines 93 to 178
Copy link
Member

Choose a reason for hiding this comment

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

I discovered an interesting case regarding JSON serialization and proto2. proto2 spec doesn't specify JSON encoding, only proto3 does. However toProto3Json can be called on both proto2 and proto3 messages. So if I have a proto2 message like

syntax = "proto2";

message Test {
  optional int32 a = 1 [default = 123];
}

I can convert this to proto3 JSON using toProto3Json, but in this PR we assume proto3 defaults -- i.e. for numeric fields the default to be 0. This doesn't hold when the message syntax is proto2. As a result output of this code is somewhat surprising:

Test test1 = Test.create()..a=0;
print(test1.toProto3Json()); // omits `a`

Test test2 = Test.create()..a=123;
print(test2.toProto3Json()); // generates `a`

It's possible to fix this issue and simplify the code at the same time. Here's my suggestion:

Suggested change
for (var fieldInfo in fs._infosSortedByTag) {
var value = fs._values[fieldInfo.index!];
if (value == null || (value is List && value.isEmpty)) {
continue; // It's missing, repeated, or an empty byte array.
}
dynamic jsonValue;
if (fieldInfo.isMapField) {
jsonValue = (value as PbMap).map((key, entryValue) {
var mapEntryInfo = fieldInfo as MapFieldInfo;
return MapEntry(convertToMapKey(key, mapEntryInfo.keyFieldType!),
valueToProto3Json(entryValue, mapEntryInfo.valueFieldType));
});
} else if (fieldInfo.isRepeated) {
jsonValue = (value as PbListBase)
.map((element) => valueToProto3Json(element, fieldInfo.type))
.toList();
// if the value for this field is null, this function will return the
// default value, which simplifies the need to handle null for most cases
// below
var value = fs._getField(fieldInfo.tagNumber);
bool skipField = true;
Object? jsonValue;
if (fieldInfo.isRepeated) {
if (context.emitDefaults || (value as List).isNotEmpty) {
skipField = false;
jsonValue = (value as PbListBase)
.map((element) => valueToProto3Json(element, fieldInfo.type))
.toList();
}
} else if (fieldInfo.isMapField) {
if (context.emitDefaults || (value as Map).isNotEmpty) {
skipField = false;
jsonValue = (value as PbMap).map((key, entryValue) {
var mapEntryInfo = fieldInfo as MapFieldInfo;
return MapEntry(convertToMapKey(key, mapEntryInfo.keyFieldType!),
valueToProto3Json(entryValue, mapEntryInfo.valueFieldType));
});
}
} else if (_isBytes(fieldInfo.type)) {
if (context.emitDefaults || (value as List).isNotEmpty) {
skipField = false;
if ((value as List).isEmpty) {
jsonValue = null;
} else {
jsonValue = valueToProto3Json(value, fieldInfo.type);
}
}
} else if (fieldInfo.isEnum) {
// For enums, the default value is the first value listed in the enum's type definition
final defaultEnum = fieldInfo.enumValues!.first;
if (context.emitDefaults ||
(value as ProtobufEnum).name != defaultEnum.name) {
skipField = false;
jsonValue = valueToProto3Json(value, fieldInfo.type);
}
} else if (fieldInfo.isGroupOrMessage) {
final originalValue = fs._values[fieldInfo.index!];
if (context.emitDefaults || originalValue != null) {
skipField = false;
if (originalValue == null) {
jsonValue = null;
} else {
jsonValue = valueToProto3Json(value, fieldInfo.type);
}
}
} else if (PbFieldType._baseType(fieldInfo.type) ==
PbFieldType._STRING_BIT) {
if (context.emitDefaults || (value as String).isNotEmpty) {
skipField = false;
jsonValue = valueToProto3Json(value, fieldInfo.type);
}
} else if (PbFieldType._baseType(fieldInfo.type) == PbFieldType._BOOL_BIT) {
if (context.emitDefaults || value != false) {
skipField = false;
jsonValue = valueToProto3Json(value, fieldInfo.type);
}
} else if (value is Int64) {
if (context.emitDefaults || !value.isZero) {
skipField = false;
jsonValue = valueToProto3Json(value, fieldInfo.type);
}
} else if (value is int) {
if (context.emitDefaults || value != 0) {
skipField = false;
jsonValue = valueToProto3Json(value, fieldInfo.type);
}
} else if (value is double) {
if (context.emitDefaults || value != 0.0) {
skipField = false;
jsonValue = valueToProto3Json(value, fieldInfo.type);
}
} else {
jsonValue = valueToProto3Json(value, fieldInfo.type);
throw JsonSerializationException(
'Unexpected field type for proto3 JSON serialization ${fieldInfo.type}');
}
if (!skipField) {
result[fieldInfo.name] = jsonValue;
}
result[fieldInfo.name] = jsonValue;
}
for (var fieldInfo in fs._infosSortedByTag) {
// Value of the field, or the default for the field if the field is not set
final dynamic value = fs._getField(fieldInfo.tagNumber);
// Whether the field should be skipped. In proto3 JSON encoding default
// values are omitted by default.
// (https://developers.google.com/protocol-buffers/docs/proto3#json)
bool skipField = true;
// Serialized JSON value for the field. Only set and used when `skipField`
// is false.
Object? jsonValue;
if (fieldInfo.isRepeated) {
if (context.emitDefaults || value != fieldInfo.readonlyDefault) {
skipField = false;
jsonValue = (value as PbListBase)
.map((element) => valueToProto3Json(element, fieldInfo.type))
.toList();
}
} else if (fieldInfo.isMapField) {
if (context.emitDefaults || value != fieldInfo.readonlyDefault) {
skipField = false;
jsonValue = (value as PbMap).map((key, entryValue) {
var mapEntryInfo = fieldInfo as MapFieldInfo;
return MapEntry(convertToMapKey(key, mapEntryInfo.keyFieldType),
valueToProto3Json(entryValue, mapEntryInfo.valueFieldType));
});
}
} else if (_isBytes(fieldInfo.type)) {
if (context.emitDefaults || !_areListsEqual(value, fieldInfo.readonlyDefault)) {
skipField = false;
if ((value as List).isEmpty) {
jsonValue = null;
} else {
jsonValue = valueToProto3Json(value, fieldInfo.type);
}
}
} else if (fieldInfo.isGroupOrMessage) {
// `_FieldInfo._getField` and `_FieldInfo.readonlyDefault` return empty
// message instead of `null` when a field with message type is not set,
// see #309.
final originalValue = fs._values[fieldInfo.index!];
if (context.emitDefaults || originalValue != null) {
skipField = false;
if (originalValue == null) {
jsonValue = null;
} else {
jsonValue = valueToProto3Json(value, fieldInfo.type);
}
}
} else {
// enum, string, bytes, numerics, map
if (context.emitDefaults || value != fieldInfo.readonlyDefault) {
skipField = false;
jsonValue = valueToProto3Json(value, fieldInfo.type);
}
}
if (!skipField) {
result[fieldInfo.name] = jsonValue;
}
}

I've also added some comments.

You'll notice that two tests will break with this change, but if you look at the test proto you'll see that we actually set the default value as 42 in those tests, so actually we fix the test. We should update the expects.

// Extensions and unknown fields are not encoded by proto3 JSON.
return result;
Expand Down
132 changes: 132 additions & 0 deletions protobuf/test/json_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ library json_test;
import 'dart:convert';

import 'package:fixnum/fixnum.dart' show Int64;
import 'package:protobuf/protobuf.dart';
import 'package:test/test.dart';

import 'mock_util.dart' show T, mockEnumValues;
Expand All @@ -16,6 +17,32 @@ void main() {
..str = 'hello'
..int32s.addAll(<int>[1, 2, 3]);

final double doubleZeroVal = 0;
final exampleAllDefaults = T()
..val = 0
..str = ''
..child = T()
..int64 = Int64(0)
..enm = ProtobufEnum(1, 'a')
..dbl = doubleZeroVal
..bl = false
..byts = <int>[];
exampleAllDefaults.int32s.addAll(<int>[]);
exampleAllDefaults.mp.addAll(<String, String>{});

final double doubleSetVal = 1.34;
final exampleAllSet = T()
..val = 32
..str = 'the-string'
..child = example
..int64 = Int64(78)
..enm = ProtobufEnum(1, 'b')
..dbl = doubleSetVal
..bl = true
..byts = <int>[46, 28];
exampleAllSet.int32s.addAll(<int>[3, 4, 6]);
exampleAllSet.mp.addAll(<String, String>{'k1': 'v2'});

test('testProto3JsonEnum', () {
// No enum value specified.
expect(example.hasEnm, isFalse);
Expand Down Expand Up @@ -115,6 +142,111 @@ void main() {
final decoded = T()..mergeFromJsonMap(encoded);
expect(decoded.int64, value);
});

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

test('testToProto3JsonNoFieldsSet', () {
final json = jsonEncode(T().toProto3Json());
expect(json.contains('{"val":42}'), isTrue);
final Map m = jsonDecode(json);
expect(m.length, 1);
});

test('testToProto3JsonFieldsSetToDefaults', () {
final json = jsonEncode(exampleAllDefaults.toProto3Json());
expect(json.contains('"child":{"val":42}'), isTrue);
final Map m = jsonDecode(json);
expect(m.isEmpty, isFalse);
});

test('testToProto3JsonFieldsSetToValues', () {
// verify the expected child is present
final json = jsonEncode(exampleAllSet.toProto3Json());
final expectedChild = '{"val":123,"str":"hello","int32s":[1,2,3]}';
expect(json.contains('"child":$expectedChild'), isTrue);
final jsonNoChild = json.replaceAll(expectedChild, '');

// verify the remaining parent object is accurate
checkExampleAllParentSetValues(jsonNoChild);
});

test('testToProto3JsonEmitDefaults', () {
final json = jsonEncode(example.toProto3Json(emitDefaults: true));
checkProto3JsonMap(jsonDecode(json), 10);
});

test('testToProto3JsonEmitDefaultsNoFieldsSet', () {
final json = jsonEncode(T().toProto3Json(emitDefaults: true));
expect(json.contains('"val":42,'), isTrue);
expect(json.contains('"str":"",'), isTrue);
expect(json.contains('"child":null,'), isTrue);
expect(json.contains('"int32s":[],'), isTrue);
expect(json.contains('"int64":"0",'), isTrue);
expect(json.contains('"enm":"a",'), isTrue);
expect(json.contains('"dbl":0.0,'), isTrue);
expect(json.contains('"bl":false,'), isTrue);
expect(json.contains('"byts":null'), isTrue);
expect(json.contains('"mp":{}'), isTrue);
});

test('testToProto3JsonEmitDefaultsFieldsSetToDefaults', () {
// verify the default child is present
final json =
jsonEncode(exampleAllDefaults.toProto3Json(emitDefaults: true));
final defaultChild =
'{"val":42,"str":"","child":null,"int32s":[],"int64":"0","enm":"a","dbl":0.0,"bl":false,"byts":null,"mp":{}}';
expect(json.contains('"child":$defaultChild'), isTrue);
final jsonNoChild = json.replaceAll(defaultChild, '');

// verify the remaining parent object is accurate
checkExampleAllParentSetDefaults(jsonNoChild);
});

test('testToProto3JsonEmitDefaultsFieldsSetToValues', () {
// verify the expected child is present
final json = jsonEncode(exampleAllSet.toProto3Json(emitDefaults: true));
final expectedChild =
'{"val":123,"str":"hello","child":null,"int32s":[1,2,3],"int64":"0","enm":"a","dbl":0.0,"bl":false,"byts":null,"mp":{}}';
expect(json.contains('"child":$expectedChild'), isTrue);
final jsonNoChild = json.replaceAll(expectedChild, '');

// verify the remaining parent object is accurate
checkExampleAllParentSetValues(jsonNoChild);
});
}

void checkExampleAllParentSetDefaults(String json) {
expect(json.contains('"val":0,'), isTrue);
expect(json.contains('"str":"",'), isTrue);
expect(json.contains('"int32s":[],'), isTrue);
expect(json.contains('"int64":"0",'), isTrue);
expect(json.contains('"enm":"a",'), isTrue);
expect(json.contains('"dbl":0.0,'), isTrue);
expect(json.contains('"bl":false,'), isTrue);
expect(json.contains('"byts":null'), isTrue);
expect(json.contains('"mp":{}'), isTrue);
}

void checkExampleAllParentSetValues(String json) {
expect(json.contains('"val":32,'), isTrue);
expect(json.contains('"str":"the-string",'), isTrue);
expect(json.contains('"int32s":[3,4,6],'), isTrue);
expect(json.contains('"int64":"78",'), isTrue);
expect(json.contains('"enm":"b",'), isTrue);
expect(json.contains('"dbl":1.34,'), isTrue);
expect(json.contains('"bl":true,'), isTrue);
expect(json.contains('"byts":"Lhw="'), isTrue);
expect(json.contains('"mp":{"k1":"v2"}'), 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
15 changes: 13 additions & 2 deletions protobuf/test/map_mixin_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,18 @@ void main() {

expect(r.isEmpty, false);
expect(r.isNotEmpty, true);
expect(r.keys, ['val', 'str', 'child', 'int32s', 'int64', 'enm']);
expect(r.keys, [
'val',
'str',
'child',
'int32s',
'int64',
'enm',
'dbl',
'bl',
'byts',
'mp'
]);

expect(r['val'], 42);
expect(r['str'], '');
Expand All @@ -42,7 +53,7 @@ void main() {
expect(r['int32s'], []);

var v = r.values;
expect(v.length, 6);
expect(v.length, 10);
expect(v.first, 42);
expect(v.toList()[1], '');
expect(v.toList()[3].toString(), '[]');
Expand Down
19 changes: 18 additions & 1 deletion protobuf/test/mock_util.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ BuilderInfo mockInfo(String className, CreateBuilderFunc create) {
..e(7, 'enm', PbFieldType.OE,
defaultOrMaker: mockEnumValues.first,
valueOf: (i) => mockEnumValues.firstWhereOrNull((e) => e.value == i),
enumValues: mockEnumValues);
enumValues: mockEnumValues)
..a(8, 'dbl', PbFieldType.OD)
..aOB(9, 'bl')
..a<int>(10, 'byts', PbFieldType.OY)
..m<String, String>(11, 'mp',
keyFieldType: PbFieldType.OS, valueFieldType: PbFieldType.OS);
}

/// A minimal protobuf implementation for testing.
Expand All @@ -49,6 +54,18 @@ abstract class MockMessage extends GeneratedMessage {

ProtobufEnum get enm => $_getN(5);
bool get hasEnm => $_has(5);
set enm(x) => setField(7, x);

double get dbl => $_get(6, 0.0);
set dbl(x) => setField(8, x);

bool get bl => $_get(7, false);
set bl(x) => setField(9, x);

List<int> get byts => $_get(8, []);
set byts(x) => setField(10, x);

Map<String, String> get mp => $_getMap(9);
Copy link
Member

Choose a reason for hiding this comment

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

It seems like you don't try to set the map field in the tests. Is there a reason for this? To make sure we handle maps correctly we should set the map field too.

Copy link
Author

@jmartin127 jmartin127 Apr 7, 2022

Choose a reason for hiding this comment

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

The map value is getting set, just in a different way... For consistency, I followed the existing example of how the int32s are set using the addAll function.

See here for an example of setting/testing the map field.
https://github.com/google/protobuf.dart/pull/592/files#diff-f9dfd1b28cb8ed5241edf3d57212698aeb5c6b3faa64d982213236cba5bd89f8R44

With this in mind, it is currently being tested, but if it would be better to set the map differently, just let me know.


@override
GeneratedMessage clone() {
Expand Down
Loading