Skip to content

Commit b761358

Browse files
biggs0125natebiggsosa1
authored
Support unknown json data and add internal set/clear field methods for generated accessors. (#918)
Add support for unknown fields to JSON (de-)serialized proto messages. This allows data to be roundtripped from a server via this format without dropping fields when the client doesn't contain metadata for some fields. Store the data as decoded but unparsed json and append it back to the message when re-serializing a message. Ignore unknown json data for equality/hashing to preserve existing behavior for users. Taking this data into account would change the result of existing `==` and `hashCode` calls. By separating out `$_setField` and `$_clearField` this allows one to differentiate reflective uses of `setField` and `clearField` (i.e. from user code, usually with the result of `getTagNumber`) vs. internal uses from the generated accessors. Fixes #49. --- cl/611522109 cl/612283849 --------- Co-authored-by: Nate Biggs <[email protected]> Co-authored-by: Ömer Sinan Ağacan <[email protected]>
1 parent 1822b81 commit b761358

18 files changed

+272
-190
lines changed

protobuf/CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,20 @@
2525
To migrate, use `PbMap.unmodifiable(map.keyFieldType, map.valueFieldType)`
2626
instead of `PbMap.unmodifiable(map)`. ([#902])
2727

28+
* Messages deserialized from JSON now generate the unknown fields when
29+
serialized as JSON.
30+
31+
Note that, as before, unknown fields in JSON messages are not stored in the
32+
`unknownFields` of the message. They are only used by the JSON serializers to
33+
support roundtripping.
34+
35+
([#49], [#918])
36+
2837
[#738]: https://github.com/google/protobuf.dart/issues/738
2938
[#896]: https://github.com/google/protobuf.dart/issues/896
3039
[#902]: https://github.com/google/protobuf.dart/issues/902
40+
[#49]: https://github.com/google/protobuf.dart/issues/49
41+
[#918]: https://github.com/google/protobuf.dart/pulls/918
3142

3243
## 3.1.0
3344

protobuf/lib/meta.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ const GeneratedMessage_reservedNames = <String>[
5656
'writeToCodedBufferWriter',
5757
'writeToJson',
5858
'writeToJsonMap',
59+
r'$_clearField',
5960
r'$_ensure',
6061
r'$_get',
6162
r'$_getI64',
@@ -72,6 +73,7 @@ const GeneratedMessage_reservedNames = <String>[
7273
r'$_setBool',
7374
r'$_setBytes',
7475
r'$_setDouble',
76+
r'$_setField',
7577
r'$_setFloat',
7678
r'$_setInt64',
7779
r'$_setSignedInt32',

protobuf/lib/src/protobuf/field_set.dart

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ class _FieldSet {
3333
/// Contains all the unknown fields, or null if there aren't any.
3434
UnknownFieldSet? _unknownFields;
3535

36+
/// Contains unknown data for messages deserialized from json.
37+
Map<String, dynamic>? _unknownJsonData;
38+
3639
/// Encodes whether `this` has been frozen, and if so, also memoizes the
3740
/// hash code.
3841
///
@@ -105,6 +108,7 @@ class _FieldSet {
105108
if (_isReadOnly) return UnknownFieldSet.emptyUnknownFieldSet;
106109
_unknownFields = UnknownFieldSet();
107110
}
111+
_unknownJsonData = null;
108112
return _unknownFields!;
109113
}
110114

@@ -531,6 +535,8 @@ class _FieldSet {
531535
if (_unknownFields != o._unknownFields) return false;
532536
}
533537

538+
// Ignore _unknownJsonData to preserve existing equality behavior.
539+
534540
return true;
535541
}
536542

@@ -597,6 +603,8 @@ class _FieldSet {
597603
// Hash with unknown fields.
598604
hash = _HashUtils._combine(hash, _unknownFields?.hashCode ?? 0);
599605

606+
// Ignore _unknownJsonData to preserve existing hashing behavior.
607+
600608
if (_isReadOnly) {
601609
_frozenState = hash;
602610
}
@@ -682,6 +690,11 @@ class _FieldSet {
682690
} else {
683691
out.write(UnknownFieldSet().toString());
684692
}
693+
694+
final unknownJsonData = _unknownJsonData;
695+
if (unknownJsonData != null) {
696+
out.write(unknownJsonData.toString());
697+
}
685698
}
686699

687700
/// Merges the contents of the [other] into this message.
@@ -713,6 +726,15 @@ class _FieldSet {
713726
if (otherUnknownFields != null) {
714727
_ensureUnknownFields().mergeFromUnknownFieldSet(otherUnknownFields);
715728
}
729+
730+
final otherUnknownJsonData = other._unknownJsonData;
731+
if (otherUnknownJsonData != null) {
732+
final newUnknownJsonData =
733+
Map<String, dynamic>.from(_unknownJsonData ?? {});
734+
otherUnknownJsonData
735+
.forEach((key, value) => newUnknownJsonData[key] = value);
736+
_unknownJsonData = newUnknownJsonData.isEmpty ? null : newUnknownJsonData;
737+
}
716738
}
717739

718740
void _mergeField(FieldInfo otherFi, fieldValue, {required bool isExtension}) {
@@ -868,6 +890,11 @@ class _FieldSet {
868890
_ensureUnknownFields()._fields.addAll(originalUnknownFields._fields);
869891
}
870892

893+
final unknownJsonData = original._unknownJsonData;
894+
if (unknownJsonData != null) {
895+
_unknownJsonData = Map.from(unknownJsonData);
896+
}
897+
871898
_oneofCases?.addAll(original._oneofCases!);
872899
}
873900
}

protobuf/lib/src/protobuf/generated_message.dart

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,10 @@ abstract class GeneratedMessage {
173173
}
174174

175175
/// Serialize the message as the protobuf binary format.
176+
///
177+
/// Unknown field data, data for which there is no metadata for the associated
178+
/// field, will only be included if this message was deserialized from the
179+
/// same wire format.
176180
Uint8List writeToBuffer() {
177181
final out = CodedBufferWriter();
178182
writeToCodedBufferWriter(out);
@@ -211,6 +215,10 @@ abstract class GeneratedMessage {
211215
/// Returns the JSON encoding of this message as a Dart [Map].
212216
///
213217
/// The encoding is described in [GeneratedMessage.writeToJson].
218+
///
219+
/// Unknown field data, data for which there is no metadata for the associated
220+
/// field, will only be included if this message was deserialized from the
221+
/// same wire format.
214222
Map<String, dynamic> writeToJsonMap() => _writeToJsonMap(_fieldSet);
215223

216224
/// Returns a JSON string that encodes this message.
@@ -226,6 +234,10 @@ abstract class GeneratedMessage {
226234
/// represented as their integer value.
227235
///
228236
/// For the proto3 JSON format use: [toProto3Json].
237+
///
238+
/// Unknown field data, data for which there is no metadata for the associated
239+
/// field, will only be included if this message was deserialized from the
240+
/// same wire format.
229241
String writeToJson() => jsonEncode(writeToJsonMap());
230242

231243
/// Returns an Object representing Proto3 JSON serialization of `this`.
@@ -241,6 +253,9 @@ abstract class GeneratedMessage {
241253
/// The [typeRegistry] is be used for encoding `Any` messages. If an `Any`
242254
/// message encoding a type not in [typeRegistry] is encountered, an
243255
/// error is thrown.
256+
///
257+
/// Unknown field data, data for which there is no metadata for the associated
258+
/// field, will not be included.
244259
Object? toProto3Json(
245260
{TypeRegistry typeRegistry = const TypeRegistry.empty()}) =>
246261
_writeToProto3Json(_fieldSet, typeRegistry);
@@ -509,6 +524,17 @@ abstract class GeneratedMessage {
509524
/// @nodoc
510525
void $_setInt64(int index, Int64 value) => _fieldSet._$set(index, value);
511526

527+
/// For generated code only. Separate from [setField] to distinguish
528+
/// reflective accesses.
529+
/// @nodoc
530+
void $_setField(int tagNumber, Object value) =>
531+
_fieldSet._setField(tagNumber, value);
532+
533+
/// For generated code only. Separate from [clearField] to distinguish
534+
/// reflective accesses.
535+
/// @nodoc
536+
void $_clearField(int tagNumber) => _fieldSet._clearField(tagNumber);
537+
512538
// Support for generating a read-only default singleton instance.
513539

514540
static final Map<Function?, _SingletonMaker<GeneratedMessage>>

protobuf/lib/src/protobuf/json.dart

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,12 @@ Map<String, dynamic> _writeToJsonMap(_FieldSet fs) {
8989
result['$tagNumber'] = convertToMap(value, fi.type);
9090
}
9191
}
92+
final unknownJsonData = fs._unknownJsonData;
93+
if (unknownJsonData != null) {
94+
unknownJsonData.forEach((key, value) {
95+
result[key] = value;
96+
});
97+
}
9298
return result;
9399
}
94100

@@ -102,9 +108,11 @@ void _mergeFromJsonMap(
102108
for (final key in keys) {
103109
var fi = meta.byTagAsString[key];
104110
if (fi == null) {
105-
if (registry == null) continue; // Unknown tag; skip
106-
fi = registry.getExtension(fs._messageName, int.parse(key));
107-
if (fi == null) continue; // Unknown tag; skip
111+
fi = registry?.getExtension(fs._messageName, int.parse(key));
112+
if (fi == null) {
113+
(fs._unknownJsonData ??= {})[key] = json[key];
114+
continue;
115+
}
108116
}
109117
if (fi.isMapField) {
110118
_appendJsonMap(

protobuf/test/json_test.dart

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ void main() {
8787
checkJsonMap(m);
8888
});
8989

90-
test('testMergeFromJson', () {
90+
test('testWriteToJsonMap', () {
9191
final t = T();
9292
t.mergeFromJson('''{"1": 123, "2": "hello"}''');
9393
checkMessage(t);
@@ -118,13 +118,21 @@ void main() {
118118
final decoded = T()..mergeFromJsonMap(encoded);
119119
expect(decoded.int64, value);
120120
});
121+
122+
test('testJsonMapWithUnknown', () {
123+
final m = example.writeToJsonMap();
124+
m['9999'] = 'world';
125+
final t = T()..mergeFromJsonMap(m);
126+
checkJsonMap(t.writeToJsonMap(), unknownFields: {'9999': 'world'});
127+
});
121128
}
122129

123-
void checkJsonMap(Map m) {
124-
expect(m.length, 3);
130+
void checkJsonMap(Map m, {Map<String, dynamic>? unknownFields}) {
131+
expect(m.length, 3 + (unknownFields?.length ?? 0));
125132
expect(m['1'], 123);
126133
expect(m['2'], 'hello');
127134
expect(m['4'], [1, 2, 3]);
135+
unknownFields?.forEach((k, v) => expect(m[k], v));
128136
}
129137

130138
void checkMessage(T t) {

protoc_plugin/lib/src/generated/dart_options.pb.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ class DartMixin extends $pb.GeneratedMessage {
8282
@$pb.TagNumber(1)
8383
$core.bool hasName() => $_has(0);
8484
@$pb.TagNumber(1)
85-
void clearName() => clearField(1);
85+
void clearName() => $_clearField(1);
8686

8787
/// A URI pointing to the Dart library that defines the mixin.
8888
/// The generated Dart code will use this in an import statement.
@@ -96,7 +96,7 @@ class DartMixin extends $pb.GeneratedMessage {
9696
@$pb.TagNumber(2)
9797
$core.bool hasImportFrom() => $_has(1);
9898
@$pb.TagNumber(2)
99-
void clearImportFrom() => clearField(2);
99+
void clearImportFrom() => $_clearField(2);
100100

101101
/// The name of another mixin to be applied ahead of this one.
102102
/// The generated class for the message will inherit from all mixins
@@ -111,7 +111,7 @@ class DartMixin extends $pb.GeneratedMessage {
111111
@$pb.TagNumber(3)
112112
$core.bool hasParent() => $_has(2);
113113
@$pb.TagNumber(3)
114-
void clearParent() => clearField(3);
114+
void clearParent() => $_clearField(3);
115115
}
116116

117117
/// Defines additional Dart imports to be used with messages in this file.

0 commit comments

Comments
 (0)