Skip to content
Merged
3 changes: 3 additions & 0 deletions protobuf/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
## 5.0.0-wip

* Improve performance of `GeneratedMessage.deepCopy`. ([#742])
* Fix unknown enum handling in `GeneratedMessage.mergeFromProto3Json` when
the `ignoreUnknownFields` optional argument is `true`. ([#853])

[#742]: https://github.com/google/protobuf.dart/pull/742
[#853]: https://github.com/google/protobuf.dart/pull/853

## 4.2.0

Expand Down
39 changes: 25 additions & 14 deletions protobuf/lib/src/protobuf/proto3_json.dart
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,8 @@ void _mergeFromProto3JsonWithContext(
fieldSet._ensureWritable();

void recursionHelper(Object? json, FieldSet fieldSet) {
// Convert a JSON object to proto object. Returns `null` on unknown enum
// values when [ignoreUnknownFields] is [true].
Object? convertProto3JsonValue(Object value, FieldInfo fieldInfo) {
final fieldType = fieldInfo.type;
switch (PbFieldType.baseType(fieldType)) {
Expand All @@ -317,16 +319,14 @@ void _mergeFromProto3JsonWithContext(
throw context.parseException('Expected bool value', json);
case PbFieldType.BYTES_BIT:
if (value is String) {
Uint8List result;
try {
result = base64Decode(value);
return base64Decode(value);
} on FormatException {
throw context.parseException(
'Expected bytes encoded as base64 String',
json,
);
}
return result;
}
throw context.parseException(
'Expected bytes encoded as base64 String',
Expand Down Expand Up @@ -427,16 +427,14 @@ void _mergeFromProto3JsonWithContext(
case PbFieldType.SFIXED64_BIT:
if (value is int) return Int64(value);
if (value is String) {
Int64 result;
try {
result = Int64.parseInt(value);
return Int64.parseInt(value);
} on FormatException {
throw context.parseException(
'Expected int or stringified int',
value,
);
}
return result;
}
throw context.parseException(
'Expected int or stringified int',
Expand Down Expand Up @@ -677,8 +675,14 @@ void _mergeFromProto3JsonWithContext(
throw context.parseException('Expected a String key', subKey);
}
context.addMapIndex(subKey);
fieldValues[decodeMapKey(subKey, mapFieldInfo.keyFieldType)] =
convertProto3JsonValue(subValue, mapFieldInfo.valueFieldInfo);
final key = decodeMapKey(subKey, mapFieldInfo.keyFieldType);
final value = convertProto3JsonValue(
subValue,
mapFieldInfo.valueFieldInfo,
);
if (value != null) {
fieldValues[key] = value;
}
context.popIndex();
});
} else {
Expand All @@ -690,7 +694,10 @@ void _mergeFromProto3JsonWithContext(
for (var i = 0; i < value.length; i++) {
final entry = value[i];
context.addListIndex(i);
values.add(convertProto3JsonValue(entry, fieldInfo));
final parsedValue = convertProto3JsonValue(entry, fieldInfo);
if (parsedValue != null) {
values.add(parsedValue);
}
context.popIndex();
}
} else {
Expand All @@ -712,11 +719,15 @@ void _mergeFromProto3JsonWithContext(
original.mergeFromMessage(parsedSubMessage);
}
} else {
fieldSet._setFieldUnchecked(
meta,
fieldInfo,
convertProto3JsonValue(value, fieldInfo),
);
final parsedValue = convertProto3JsonValue(value, fieldInfo);
if (parsedValue == null) {
// Unknown enum
if (!context.ignoreUnknownFields) {
throw context.parseException('Unknown enum value', value);
}
} else {
fieldSet._setFieldUnchecked(meta, fieldInfo, parsedValue);
}
}
context.popIndex();
});
Expand Down
47 changes: 29 additions & 18 deletions protobuf/test/json_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,21 @@ import 'package:test/test.dart';
import 'mock_util.dart' show T, mockEnumValues;

void main() {
final example =
T()
..val = 123
..str = 'hello'
..int32s.addAll(<int>[1, 2, 3]);
test('mergeFromProto3Json unknown enum fields with names', () {
final example = T();

test('testProto3JsonEnum', () {
// No enum value specified.
expect(example.hasEnm, isFalse);

// Defaults to first when it doesn't exist.
expect(example.enm, equals(mockEnumValues.first));
expect((example..mergeFromProto3Json({'enm': 'a'})).enm.name, equals('a'));

// Now it's explicitly set after merging.
expect(example.hasEnm, isTrue);

expect((example..mergeFromProto3Json({'enm': 'b'})).enm.name, equals('b'));

// "c" is not a legal enum value.
expect(
() => example..mergeFromProto3Json({'enm': 'c'}),
Expand All @@ -42,21 +41,26 @@ void main() {
),
),
);

// `example` hasn't changed.
expect(example.hasEnm, isTrue);
expect(example.enm.name, equals('b'));

// "c" is not a legal enum value, but we are ignoring unknown fields, so
// default behavior is to unset `enm`, returning the default value "a"
// `enm` value shouldn't change.
expect(
(example..mergeFromProto3Json({'enm': 'c'}, ignoreUnknownFields: true))
.enm
.name,
equals('a'),
equals('b'),
);
expect(example.hasEnm, isFalse);
expect(example.hasEnm, isTrue);
});

test('mergeFromProto3Json unknown enum fields with indices', () {
// Similar to above, but with indices.
final example = T();

// Same for index values...
expect((example..mergeFromProto3Json({'enm': 2})).enm.name, 'b');
expect(
() => example..mergeFromProto3Json({'enm': 3}),
Expand All @@ -69,34 +73,35 @@ void main() {
),
),
);

// `example` hasn't changed.
expect(example.hasEnm, isTrue);
expect(example.enm.name, equals('b'));

// "c" is not a legal enum value, but we are ignoring unknown fields, so
// default behavior is to unset `enm`, returning the default value "a"
// "c" is not a legal enum value, but we are ignoring unknown fields, so the
// value shouldn't change.
expect(
(example..mergeFromProto3Json({'enm': 3}, ignoreUnknownFields: true))
.enm
.name,
equals('a'),
equals('b'),
);
expect(example.hasEnm, isFalse);
expect(example.hasEnm, isTrue);
});

test('testWriteToJson', () {
final json = example.writeToJson();
final json = makeTestJson().writeToJson();
checkJsonMap(jsonDecode(json));
});

test('testWriteFrozenToJson', () {
final frozen = example.clone()..freeze();
final frozen = makeTestJson().clone()..freeze();
final json = frozen.writeToJson();
checkJsonMap(jsonDecode(json));
});

test('writeToJsonMap', () {
final Map m = example.writeToJsonMap();
final Map m = makeTestJson().writeToJsonMap();
checkJsonMap(m);
});

Expand Down Expand Up @@ -134,13 +139,19 @@ void main() {
});

test('testJsonMapWithUnknown', () {
final m = example.writeToJsonMap();
final m = makeTestJson().writeToJsonMap();
m['9999'] = 'world';
final t = T()..mergeFromJsonMap(m);
checkJsonMap(t.writeToJsonMap(), unknownFields: {'9999': 'world'});
});
}

T makeTestJson() =>
T()
..val = 123
..str = 'hello'
..int32s.addAll(<int>[1, 2, 3]);

void checkJsonMap(Map m, {Map<String, dynamic>? unknownFields}) {
expect(m.length, 3 + (unknownFields?.length ?? 0));
expect(m['1'], 123);
Expand Down
1 change: 1 addition & 0 deletions protoc_plugin/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ TEST_PROTO_LIST = \
entity \
enum_extension \
enum_name \
enum_test \
enums \
extend_unittest \
ExtensionEnumNameConflict \
Expand Down
16 changes: 16 additions & 0 deletions protoc_plugin/test/protos/enum_test.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) 2025, 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.

syntax = "proto3";

enum A {
X = 0;
Y = 1;
}

message Message {
A enum_field = 1;
map<int32, A> map_value_field = 2;
repeated A repeated_enum_field = 3;
}
41 changes: 41 additions & 0 deletions protoc_plugin/test/unknown_enums_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) 2025, 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.

import 'package:test/test.dart';

import 'gen/enum_test.pb.dart';

void main() {
// 'Z' below is an unknown enum value. Known values are 'X' and 'Y'.
group('Enum parsing in maps, lists, messages', () {
test('Parse known fields', () {
final json = {
'enumField': 'Y',
'mapValueField': {'1': 'Y'},
'repeatedEnumField': ['Y'],
};

final msg = Message();
msg.mergeFromProto3Json(json);
expect(msg.enumField, A.Y);
expect(msg.mapValueField.values.toList(), [A.Y]);
expect(msg.repeatedEnumField, [A.Y]);
});

test('Skip unknown fields', () {
final json = {
'enumField': 'Z',
'mapValueField': {'1': 'X', '2': 'Z', '3': 'Y'},
'repeatedEnumField': ['X', 'Z', 'Y'],
};

final msg = Message();
msg.enumField = A.Y;
msg.mergeFromProto3Json(json, ignoreUnknownFields: true);
expect(msg.enumField, A.Y);
expect(msg.mapValueField.values.toList(), [A.X, A.Y]);
expect(msg.repeatedEnumField, [A.X, A.Y]);
});
});
}
Loading