Skip to content

Commit 2596676

Browse files
osa1Spiritus2424
authored andcommitted
Fix unknown enum handling (google#853)
This fixes handling of unknown enum values in repeated and map fields and in top-level fields. Current behavior: - Unknown enums in map and repeated fields throw an exception. - Unknown enums in top-level fields reset the field value to the default one. With this PR we just "ignore" the values: - Unknown enums in map and repeated fields are skipped. - Unknown enums in top-level fields do not reset the field's value to the default. Fixes google#849.
1 parent 1f44d56 commit 2596676

File tree

6 files changed

+115
-32
lines changed

6 files changed

+115
-32
lines changed

protobuf/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
## 5.0.0-wip
22

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

57
[#742]: https://github.com/google/protobuf.dart/pull/742
8+
[#853]: https://github.com/google/protobuf.dart/pull/853
69

710
## 4.2.0
811

protobuf/lib/src/protobuf/proto3_json.dart

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,8 @@ void _mergeFromProto3JsonWithContext(
307307
fieldSet._ensureWritable();
308308

309309
void recursionHelper(Object? json, FieldSet fieldSet) {
310+
// Convert a JSON object to proto object. Returns `null` on unknown enum
311+
// values when [ignoreUnknownFields] is [true].
310312
Object? convertProto3JsonValue(Object value, FieldInfo fieldInfo) {
311313
final fieldType = fieldInfo.type;
312314
switch (PbFieldType.baseType(fieldType)) {
@@ -317,16 +319,14 @@ void _mergeFromProto3JsonWithContext(
317319
throw context.parseException('Expected bool value', json);
318320
case PbFieldType.BYTES_BIT:
319321
if (value is String) {
320-
Uint8List result;
321322
try {
322-
result = base64Decode(value);
323+
return base64Decode(value);
323324
} on FormatException {
324325
throw context.parseException(
325326
'Expected bytes encoded as base64 String',
326327
json,
327328
);
328329
}
329-
return result;
330330
}
331331
throw context.parseException(
332332
'Expected bytes encoded as base64 String',
@@ -427,16 +427,14 @@ void _mergeFromProto3JsonWithContext(
427427
case PbFieldType.SFIXED64_BIT:
428428
if (value is int) return Int64(value);
429429
if (value is String) {
430-
Int64 result;
431430
try {
432-
result = Int64.parseInt(value);
431+
return Int64.parseInt(value);
433432
} on FormatException {
434433
throw context.parseException(
435434
'Expected int or stringified int',
436435
value,
437436
);
438437
}
439-
return result;
440438
}
441439
throw context.parseException(
442440
'Expected int or stringified int',
@@ -677,8 +675,14 @@ void _mergeFromProto3JsonWithContext(
677675
throw context.parseException('Expected a String key', subKey);
678676
}
679677
context.addMapIndex(subKey);
680-
fieldValues[decodeMapKey(subKey, mapFieldInfo.keyFieldType)] =
681-
convertProto3JsonValue(subValue, mapFieldInfo.valueFieldInfo);
678+
final key = decodeMapKey(subKey, mapFieldInfo.keyFieldType);
679+
final value = convertProto3JsonValue(
680+
subValue,
681+
mapFieldInfo.valueFieldInfo,
682+
);
683+
if (value != null) {
684+
fieldValues[key] = value;
685+
}
682686
context.popIndex();
683687
});
684688
} else {
@@ -690,7 +694,10 @@ void _mergeFromProto3JsonWithContext(
690694
for (var i = 0; i < value.length; i++) {
691695
final entry = value[i];
692696
context.addListIndex(i);
693-
values.add(convertProto3JsonValue(entry, fieldInfo));
697+
final parsedValue = convertProto3JsonValue(entry, fieldInfo);
698+
if (parsedValue != null) {
699+
values.add(parsedValue);
700+
}
694701
context.popIndex();
695702
}
696703
} else {
@@ -712,11 +719,15 @@ void _mergeFromProto3JsonWithContext(
712719
original.mergeFromMessage(parsedSubMessage);
713720
}
714721
} else {
715-
fieldSet._setFieldUnchecked(
716-
meta,
717-
fieldInfo,
718-
convertProto3JsonValue(value, fieldInfo),
719-
);
722+
final parsedValue = convertProto3JsonValue(value, fieldInfo);
723+
if (parsedValue == null) {
724+
// Unknown enum
725+
if (!context.ignoreUnknownFields) {
726+
throw context.parseException('Unknown enum value', value);
727+
}
728+
} else {
729+
fieldSet._setFieldUnchecked(meta, fieldInfo, parsedValue);
730+
}
720731
}
721732
context.popIndex();
722733
});

protobuf/test/json_test.dart

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,21 @@ import 'package:test/test.dart';
1414
import 'mock_util.dart' show T, mockEnumValues;
1515

1616
void main() {
17-
final example =
18-
T()
19-
..val = 123
20-
..str = 'hello'
21-
..int32s.addAll(<int>[1, 2, 3]);
17+
test('mergeFromProto3Json unknown enum fields with names', () {
18+
final example = T();
2219

23-
test('testProto3JsonEnum', () {
2420
// No enum value specified.
2521
expect(example.hasEnm, isFalse);
22+
2623
// Defaults to first when it doesn't exist.
2724
expect(example.enm, equals(mockEnumValues.first));
2825
expect((example..mergeFromProto3Json({'enm': 'a'})).enm.name, equals('a'));
26+
2927
// Now it's explicitly set after merging.
3028
expect(example.hasEnm, isTrue);
3129

3230
expect((example..mergeFromProto3Json({'enm': 'b'})).enm.name, equals('b'));
31+
3332
// "c" is not a legal enum value.
3433
expect(
3534
() => example..mergeFromProto3Json({'enm': 'c'}),
@@ -42,21 +41,26 @@ void main() {
4241
),
4342
),
4443
);
44+
4545
// `example` hasn't changed.
4646
expect(example.hasEnm, isTrue);
4747
expect(example.enm.name, equals('b'));
4848

4949
// "c" is not a legal enum value, but we are ignoring unknown fields, so
50-
// default behavior is to unset `enm`, returning the default value "a"
50+
// `enm` value shouldn't change.
5151
expect(
5252
(example..mergeFromProto3Json({'enm': 'c'}, ignoreUnknownFields: true))
5353
.enm
5454
.name,
55-
equals('a'),
55+
equals('b'),
5656
);
57-
expect(example.hasEnm, isFalse);
57+
expect(example.hasEnm, isTrue);
58+
});
59+
60+
test('mergeFromProto3Json unknown enum fields with indices', () {
61+
// Similar to above, but with indices.
62+
final example = T();
5863

59-
// Same for index values...
6064
expect((example..mergeFromProto3Json({'enm': 2})).enm.name, 'b');
6165
expect(
6266
() => example..mergeFromProto3Json({'enm': 3}),
@@ -69,34 +73,35 @@ void main() {
6973
),
7074
),
7175
);
76+
7277
// `example` hasn't changed.
7378
expect(example.hasEnm, isTrue);
7479
expect(example.enm.name, equals('b'));
7580

76-
// "c" is not a legal enum value, but we are ignoring unknown fields, so
77-
// default behavior is to unset `enm`, returning the default value "a"
81+
// "c" is not a legal enum value, but we are ignoring unknown fields, so the
82+
// value shouldn't change.
7883
expect(
7984
(example..mergeFromProto3Json({'enm': 3}, ignoreUnknownFields: true))
8085
.enm
8186
.name,
82-
equals('a'),
87+
equals('b'),
8388
);
84-
expect(example.hasEnm, isFalse);
89+
expect(example.hasEnm, isTrue);
8590
});
8691

8792
test('testWriteToJson', () {
88-
final json = example.writeToJson();
93+
final json = makeTestJson().writeToJson();
8994
checkJsonMap(jsonDecode(json));
9095
});
9196

9297
test('testWriteFrozenToJson', () {
93-
final frozen = example.clone()..freeze();
98+
final frozen = makeTestJson().clone()..freeze();
9499
final json = frozen.writeToJson();
95100
checkJsonMap(jsonDecode(json));
96101
});
97102

98103
test('writeToJsonMap', () {
99-
final Map m = example.writeToJsonMap();
104+
final Map m = makeTestJson().writeToJsonMap();
100105
checkJsonMap(m);
101106
});
102107

@@ -134,13 +139,19 @@ void main() {
134139
});
135140

136141
test('testJsonMapWithUnknown', () {
137-
final m = example.writeToJsonMap();
142+
final m = makeTestJson().writeToJsonMap();
138143
m['9999'] = 'world';
139144
final t = T()..mergeFromJsonMap(m);
140145
checkJsonMap(t.writeToJsonMap(), unknownFields: {'9999': 'world'});
141146
});
142147
}
143148

149+
T makeTestJson() =>
150+
T()
151+
..val = 123
152+
..str = 'hello'
153+
..int32s.addAll(<int>[1, 2, 3]);
154+
144155
void checkJsonMap(Map m, {Map<String, dynamic>? unknownFields}) {
145156
expect(m.length, 3 + (unknownFields?.length ?? 0));
146157
expect(m['1'], 123);

protoc_plugin/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ TEST_PROTO_LIST = \
3131
entity \
3232
enum_extension \
3333
enum_name \
34+
enum_test \
3435
enums \
3536
extend_unittest \
3637
ExtensionEnumNameConflict \
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
syntax = "proto3";
6+
7+
enum A {
8+
X = 0;
9+
Y = 1;
10+
}
11+
12+
message Message {
13+
A enum_field = 1;
14+
map<int32, A> map_value_field = 2;
15+
repeated A repeated_enum_field = 3;
16+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:test/test.dart';
6+
7+
import 'gen/enum_test.pb.dart';
8+
9+
void main() {
10+
// 'Z' below is an unknown enum value. Known values are 'X' and 'Y'.
11+
group('Enum parsing in maps, lists, messages', () {
12+
test('Parse known fields', () {
13+
final json = {
14+
'enumField': 'Y',
15+
'mapValueField': {'1': 'Y'},
16+
'repeatedEnumField': ['Y'],
17+
};
18+
19+
final msg = Message();
20+
msg.mergeFromProto3Json(json);
21+
expect(msg.enumField, A.Y);
22+
expect(msg.mapValueField.values.toList(), [A.Y]);
23+
expect(msg.repeatedEnumField, [A.Y]);
24+
});
25+
26+
test('Skip unknown fields', () {
27+
final json = {
28+
'enumField': 'Z',
29+
'mapValueField': {'1': 'X', '2': 'Z', '3': 'Y'},
30+
'repeatedEnumField': ['X', 'Z', 'Y'],
31+
};
32+
33+
final msg = Message();
34+
msg.enumField = A.Y;
35+
msg.mergeFromProto3Json(json, ignoreUnknownFields: true);
36+
expect(msg.enumField, A.Y);
37+
expect(msg.mapValueField.values.toList(), [A.X, A.Y]);
38+
expect(msg.repeatedEnumField, [A.X, A.Y]);
39+
});
40+
});
41+
}

0 commit comments

Comments
 (0)