diff --git a/protobuf/CHANGELOG.md b/protobuf/CHANGELOG.md index a987ef80..209c21b9 100644 --- a/protobuf/CHANGELOG.md +++ b/protobuf/CHANGELOG.md @@ -8,8 +8,12 @@ * Some of the private `PbFieldType` members are made public, to allow using them in internal libraries. This type is for internal use only. ([#1027]) +* Improve performance of `GeneratedMessage` members: `writeToJsonMap`, + `writeToJson`, `mergeFromJson`, `mergeFromJsonMap`. ([#1028]) + [#1026]: https://github.com/google/protobuf.dart/pull/1026 [#1027]: https://github.com/google/protobuf.dart/pull/1027 +[#1028]: https://github.com/google/protobuf.dart/pull/1028 ## 4.1.1 diff --git a/protobuf/lib/src/protobuf/generated_message.dart b/protobuf/lib/src/protobuf/generated_message.dart index 35caa69b..9b5c866a 100644 --- a/protobuf/lib/src/protobuf/generated_message.dart +++ b/protobuf/lib/src/protobuf/generated_message.dart @@ -227,7 +227,7 @@ abstract class GeneratedMessage { /// Unknown field data, data for which there is no metadata for the associated /// field, will only be included if this message was deserialized from the /// same wire format. - Map writeToJsonMap() => _writeToJsonMap(_fieldSet); + Map writeToJsonMap() => json_lib.writeToJsonMap(_fieldSet); /// Returns a JSON string that encodes this message. /// @@ -246,7 +246,7 @@ abstract class GeneratedMessage { /// Unknown field data, data for which there is no metadata for the associated /// field, will only be included if this message was deserialized from the /// same wire format. - String writeToJson() => jsonEncode(writeToJsonMap()); + String writeToJson() => json_lib.writeToJsonString(_fieldSet); /// Returns an Object representing Proto3 JSON serialization of `this`. /// @@ -318,19 +318,9 @@ abstract class GeneratedMessage { String data, [ ExtensionRegistry extensionRegistry = ExtensionRegistry.EMPTY, ]) { - /// Disable lazy creation of Dart objects for a dart2js speedup. - /// This is a slight regression on the Dart VM. - /// TODO(skybrian) we could skip the reviver if we're running - /// on the Dart VM for a slight speedup. - final Map jsonMap = jsonDecode( - data, - reviver: _emptyReviver, - ); - _mergeFromJsonMap(_fieldSet, jsonMap, extensionRegistry); + json_lib.mergeFromJsonString(_fieldSet, data, extensionRegistry); } - static Object? _emptyReviver(Object? k, Object? v) => v; - /// Merges field values from a JSON object represented as a Dart map. /// /// The encoding is described in [GeneratedMessage.writeToJson]. @@ -338,7 +328,7 @@ abstract class GeneratedMessage { Map json, [ ExtensionRegistry extensionRegistry = ExtensionRegistry.EMPTY, ]) { - _mergeFromJsonMap(_fieldSet, json, extensionRegistry); + json_lib.mergeFromJsonMap(_fieldSet, json, extensionRegistry); } /// Adds an extension field value to a repeated field. diff --git a/protobuf/lib/src/protobuf/internal.dart b/protobuf/lib/src/protobuf/internal.dart index 176084d7..e3a29d07 100644 --- a/protobuf/lib/src/protobuf/internal.dart +++ b/protobuf/lib/src/protobuf/internal.dart @@ -8,14 +8,7 @@ library; import 'dart:collection' show ListBase, MapBase; -import 'dart:convert' - show - Utf8Decoder, - Utf8Encoder, - base64Decode, - base64Encode, - jsonDecode, - jsonEncode; +import 'dart:convert' show Utf8Decoder, Utf8Encoder, base64Decode, base64Encode; import 'dart:math' as math; import 'dart:typed_data' show ByteData, Endian, Uint8List; @@ -23,6 +16,7 @@ import 'package:fixnum/fixnum.dart' show Int64; import 'package:meta/meta.dart' show UseResult; import 'consts.dart'; +import 'json/json.dart' as json_lib; import 'json_parsing_context.dart'; import 'permissive_compare.dart'; import 'type_registry.dart'; @@ -45,7 +39,6 @@ part 'field_set.dart'; part 'field_type.dart'; part 'generated_message.dart'; part 'generated_service.dart'; -part 'json.dart'; part 'message_set.dart'; part 'pb_list.dart'; part 'pb_map.dart'; diff --git a/protobuf/lib/src/protobuf/json.dart b/protobuf/lib/src/protobuf/json/json.dart similarity index 85% rename from protobuf/lib/src/protobuf/json.dart rename to protobuf/lib/src/protobuf/json/json.dart index 5021fc07..05d1ac11 100644 --- a/protobuf/lib/src/protobuf/json.dart +++ b/protobuf/lib/src/protobuf/json/json.dart @@ -2,9 +2,20 @@ // 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. -part of 'internal.dart'; +import 'dart:convert' show base64Decode, base64Encode; -Map _writeToJsonMap(FieldSet fs) { +import 'package:fixnum/fixnum.dart' show Int64; + +import '../consts.dart'; +import '../internal.dart'; +import '../utils.dart'; + +// Use json_vm.dart with VM and dart2wasm, json_web.dart with dart2js. +// json_web.dart uses JS interop for parsing, and JS interop is too slow on +// Wasm. VM's patch performs better in Wasm. +export 'json_vm.dart' if (dart.library.html) 'json_web.dart'; + +Map writeToJsonMap(FieldSet fs) { dynamic convertToMap(dynamic fieldValue, int fieldType) { final baseType = PbFieldType.baseType(fieldType); @@ -61,15 +72,15 @@ Map _writeToJsonMap(FieldSet fs) { List writeMap(PbMap fieldValue, MapFieldInfo fi) => List.from( fieldValue.entries.map( (MapEntry e) => { - '${PbMap._keyFieldNumber}': convertToMap(e.key, fi.keyFieldType), - '${PbMap._valueFieldNumber}': convertToMap(e.value, fi.valueFieldType), + '$mapKeyFieldNumber': convertToMap(e.key, fi.keyFieldType), + '$mapValueFieldNumber': convertToMap(e.value, fi.valueFieldType), }, ), ); final result = {}; - for (final fi in fs._infosSortedByTag) { - final value = fs._values[fi.index!]; + for (final fi in fs.infosSortedByTag) { + final value = fs.values[fi.index!]; if (value == null || (value is List && value.isEmpty)) { continue; // It's missing, repeated, or an empty byte array. } @@ -82,18 +93,18 @@ Map _writeToJsonMap(FieldSet fs) { } result['${fi.tagNumber}'] = convertToMap(value, fi.type); } - final extensions = fs._extensions; + final extensions = fs.extensions; if (extensions != null) { - for (final tagNumber in sorted(extensions._tagNumbers)) { - final value = extensions._values[tagNumber]; + for (final tagNumber in sorted(extensions.tagNumbers)) { + final value = extensions.values[tagNumber]; if (value is List && value.isEmpty) { continue; // It's repeated or an empty byte array. } - final fi = extensions._getInfoOrNull(tagNumber)!; + final fi = extensions.getInfoOrNull(tagNumber)!; result['$tagNumber'] = convertToMap(value, fi.type); } } - final unknownJsonData = fs._unknownJsonData; + final unknownJsonData = fs.unknownJsonData; if (unknownJsonData != null) { unknownJsonData.forEach((key, value) { result[key] = value; @@ -104,20 +115,20 @@ Map _writeToJsonMap(FieldSet fs) { // Merge fields from a previously decoded JSON object. // (Called recursively on nested messages.) -void _mergeFromJsonMap( +void mergeFromJsonMap( FieldSet fs, Map json, ExtensionRegistry? registry, ) { - fs._ensureWritable(); + fs.ensureWritable(); final keys = json.keys; - final meta = fs._meta; + final meta = fs.meta; for (final key in keys) { var fi = meta.byTagAsString[key]; if (fi == null) { - fi = registry?.getExtension(fs._messageName, int.parse(key)); + fi = registry?.getExtension(fs.messageName, int.parse(key)); if (fi == null) { - (fs._unknownJsonData ??= {})[key] = json[key]; + (fs.unknownJsonData ??= {})[key] = json[key]; continue; } } @@ -144,7 +155,7 @@ void _appendJsonList( FieldInfo fi, ExtensionRegistry? registry, ) { - final repeated = fi._ensureRepeatedField(meta, fs); + final repeated = fi.ensureRepeatedField(meta, fs); // Micro optimization. Using "for in" generates the following and iterator // alloc: // for (t1 = J.get$iterator$ax(json), t2 = fi.tagNumber, t3 = fi.type, @@ -175,23 +186,23 @@ void _appendJsonMap( ExtensionRegistry? registry, ) { final entryMeta = fi.mapEntryBuilderInfo; - final map = fi._ensureMapField(meta, fs); + final map = fi.ensureMapField(meta, fs); for (final jsonEntryDynamic in jsonList) { final jsonEntry = jsonEntryDynamic as Map; final entryFieldSet = FieldSet(null, entryMeta); final convertedKey = _convertJsonValue( entryMeta, entryFieldSet, - jsonEntry['${PbMap._keyFieldNumber}'], - PbMap._keyFieldNumber, + jsonEntry['$mapKeyFieldNumber'], + mapKeyFieldNumber, fi.keyFieldType, registry, ); var convertedValue = _convertJsonValue( entryMeta, entryFieldSet, - jsonEntry['${PbMap._valueFieldNumber}'], - PbMap._valueFieldNumber, + jsonEntry['$mapValueFieldNumber'], + mapValueFieldNumber, fi.valueFieldType, registry, ); @@ -223,10 +234,10 @@ void _setJsonField( // Therefore we run _validateField for debug builds only to validate // correctness of conversion. assert(() { - fs._validateField(fi, value); + fs.validateField(fi, value); return true; }()); - fs._setFieldUnchecked(meta, fi, value); + fs.setFieldUnchecked(meta, fi, value); } /// Converts [value] from the JSON format to the Dart data type suitable for @@ -298,7 +309,7 @@ dynamic _convertJsonValue( // The following call will return null if the enum value is unknown. // In that case, we want the caller to ignore this value, so we return // null from this method as well. - return meta._decodeEnum(tagNumber, registry, value); + return meta.decodeEnum(tagNumber, registry, value); } expectedType = 'int or stringified int'; break; @@ -333,8 +344,8 @@ dynamic _convertJsonValue( case PbFieldType.MESSAGE_BIT: if (value is Map) { final messageValue = value as Map; - final subMessage = meta._makeEmptyMessage(tagNumber, registry); - _mergeFromJsonMap(subMessage._fieldSet, messageValue, registry); + final subMessage = meta.makeEmptyMessage(tagNumber, registry); + mergeFromJsonMap(subMessage.fieldSet, messageValue, registry); return subMessage; } expectedType = 'nested message or group'; diff --git a/protobuf/lib/src/protobuf/json/json_vm.dart b/protobuf/lib/src/protobuf/json/json_vm.dart new file mode 100644 index 00000000..831756ab --- /dev/null +++ b/protobuf/lib/src/protobuf/json/json_vm.dart @@ -0,0 +1,23 @@ +// 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 'dart:convert' show jsonDecode, jsonEncode; + +import '../internal.dart'; +import 'json.dart'; + +String writeToJsonString(FieldSet fs) => jsonEncode(writeToJsonMap(fs)); + +/// Merge fields from a [json] string. +void mergeFromJsonString( + FieldSet fs, + String json, + ExtensionRegistry? registry, +) { + final jsonMap = jsonDecode(json); + if (jsonMap is! Map) { + throw ArgumentError.value(json, 'json', 'Does not parse to a JSON object.'); + } + mergeFromJsonMap(fs, jsonMap, registry); +} diff --git a/protobuf/lib/src/protobuf/json/json_web.dart b/protobuf/lib/src/protobuf/json/json_web.dart new file mode 100644 index 00000000..b0a8e2ec --- /dev/null +++ b/protobuf/lib/src/protobuf/json/json_web.dart @@ -0,0 +1,471 @@ +// 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 'dart:convert' show base64Decode, base64Encode; +import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; + +import 'package:fixnum/fixnum.dart' show Int64; + +import '../consts.dart'; +import '../internal.dart'; +import '../utils.dart'; + +@JS('JSON') +extension type _JSON._(JSObject _) implements JSObject { + @JS('JSON.stringify') + external static JSString _stringify(JSObject value); + + @JS('JSON.parse') + external static JSAny? _parse(JSString text); +} + +@JS('Number') +extension type _Number._(JSObject _) implements JSObject { + @JS('Number.isInteger') + external static bool _isInteger(JSAny value); +} + +@JS('Object.keys') +external JSArray _objectKeys(JSObject obj); + +@JS('Object.prototype') +external JSObject get _objectPrototype; + +@JS('Object.getPrototypeOf') +external JSObject _getPrototypeOf(JSAny obj); + +extension on JSAny { + /// Returns this typed as [T] while omitting the `as` cast. For use after an + /// `isA` check. + @pragma('dart2js:as:trust') + @pragma('dart2js:prefer-inline') + T _as() => this as T; +} + +String writeToJsonString(FieldSet fs) { + final rawJs = _writeToRawJs(fs); + return _JSON._stringify(rawJs).toDart; +} + +JSObject _writeToRawJs(FieldSet fs) { + JSAny convertToRawJs(dynamic fieldValue, int fieldType) { + final baseType = PbFieldType.baseType(fieldType); + + if (PbFieldType.isRepeated(fieldType)) { + final PbList list = fieldValue; + final length = list.length; + final jsArray = JSArray.withLength(length); + for (var i = 0; i < length; i++) { + final entry = list[i]; + jsArray[i] = convertToRawJs(entry, baseType); + } + return jsArray; + } + + switch (baseType) { + case PbFieldType.INT32_BIT: + case PbFieldType.SINT32_BIT: + case PbFieldType.UINT32_BIT: + case PbFieldType.FIXED32_BIT: + case PbFieldType.SFIXED32_BIT: + final int value = fieldValue; + return value.toJS; + + case PbFieldType.BOOL_BIT: + final bool value = fieldValue; + return value.toJS; + + case PbFieldType.STRING_BIT: + final String value = fieldValue; + return value.toJS; + + case PbFieldType.FLOAT_BIT: + case PbFieldType.DOUBLE_BIT: + final double value = fieldValue; + if (value.isNaN) { + return nan.toJS; + } + if (value.isInfinite) { + return value.isNegative ? negativeInfinity.toJS : infinity.toJS; + } + if (value.toInt() == value) { + return value.toInt().toJS; + } + return value.toJS; + + case PbFieldType.BYTES_BIT: + // Encode 'bytes' as a base64-encoded string. + final List value = fieldValue; + return base64Encode(value).toJS; + + case PbFieldType.ENUM_BIT: + final ProtobufEnum enum_ = fieldValue; + return enum_.value.toJS; // assume |value| < 2^52 + + case PbFieldType.INT64_BIT: + case PbFieldType.SINT64_BIT: + case PbFieldType.SFIXED64_BIT: + final Int64 int_ = fieldValue; + return int_.toString().toJS; + + case PbFieldType.UINT64_BIT: + case PbFieldType.FIXED64_BIT: + final Int64 int_ = fieldValue; + return int_.toStringUnsigned().toJS; + + case PbFieldType.GROUP_BIT: + case PbFieldType.MESSAGE_BIT: + final GeneratedMessage msg = fieldValue; + return _writeToRawJs(msg.fieldSet); + + default: + throw UnsupportedError('Unknown type $fieldType'); + } + } + + JSArray writeMap(PbMap fieldValue, MapFieldInfo fi) { + final length = fieldValue.entries.length; + final jsArray = JSArray.withLength(length); + var index = 0; + for (final entry in fieldValue.entries) { + final entryJsObj = JSObject(); + entryJsObj.setProperty( + mapKeyFieldNumber.toJS, + convertToRawJs(entry.key, fi.keyFieldType), + ); + entryJsObj.setProperty( + mapValueFieldNumber.toJS, + convertToRawJs(entry.value, fi.valueFieldType), + ); + jsArray[index] = entryJsObj; + index++; + } + return jsArray; + } + + final result = JSObject(); + for (final fi in fs.infosSortedByTag) { + final value = fs.values[fi.index!]; + if (value == null || (value is List && value.isEmpty)) { + continue; // It's missing, repeated, or an empty byte array. + } + if (PbFieldType.isMapField(fi.type)) { + result.setProperty( + fi.tagNumber.toJS, + writeMap(value, fi as MapFieldInfo), + ); + continue; + } + result.setProperty(fi.tagNumber.toJS, convertToRawJs(value, fi.type)); + } + final extensions = fs.extensions; + if (extensions != null) { + for (final tagNumber in sorted(extensions.tagNumbers)) { + final value = extensions.values[tagNumber]; + if (value is List && value.isEmpty) { + continue; // It's repeated or an empty byte array. + } + final fi = extensions.getInfoOrNull(tagNumber)!; + result.setProperty(tagNumber.toJS, convertToRawJs(value, fi.type)); + } + } + final unknownJsonData = fs.unknownJsonData; + if (unknownJsonData != null) { + unknownJsonData.forEach((key, value) { + result.setProperty(key.toJS, value); + }); + } + return result; +} + +/// Merge fields from a [json] string. +void mergeFromJsonString( + FieldSet fs, + String json, + ExtensionRegistry? registry, +) { + final JSAny? parsed; + try { + parsed = _JSON._parse(json.toJS); + } catch (e) { + throw FormatException(e.toString()); + } + if (parsed == null || !parsed.isA()) { + throw ArgumentError.value(json, 'json', 'Does not parse to a JSON object.'); + } + _mergeFromRawJsMap(fs, parsed._as(), registry); +} + +void _mergeFromRawJsMap( + FieldSet fs, + JSObject json, + ExtensionRegistry? registry, +) { + fs.ensureWritable(); + + final meta = fs.meta; + final keys = _objectKeys(json); + final length = keys.length; + for (var i = 0; i < length; i++) { + final jsKey = keys[i]; + final key = jsKey.toDart; + var fi = meta.byTagAsString[key]; + if (fi == null) { + fi = registry?.getExtension(fs.messageName, int.parse(key)); + if (fi == null) { + (fs.unknownJsonData ??= {})[key] = json.getProperty(jsKey); + continue; + } + } + if (fi.isMapField) { + _appendRawJsMap( + meta, + fs, + json.getProperty>(jsKey), + fi as MapFieldInfo, + registry, + ); + } else if (fi.isRepeated) { + _appendRawJsList( + meta, + fs, + json.getProperty>(jsKey), + fi, + registry, + ); + } else { + _setRawJsField(meta, fs, json.getProperty(jsKey), fi, registry); + } + } +} + +void _appendRawJsList( + BuilderInfo meta, + FieldSet fs, + JSArray jsonList, + FieldInfo fi, + ExtensionRegistry? registry, +) { + final repeated = fi.ensureRepeatedField(meta, fs); + // Micro optimization. Using "for in" generates the following and iterator + // alloc: + // for (t1 = J.get$iterator$ax(json), t2 = fi.tagNumber, t3 = fi.type, + // t4 = J.getInterceptor$ax(repeated); t1.moveNext$0();) + final length = jsonList.length; + for (var i = 0; i < length; i++) { + final value = jsonList[i]; + var convertedValue = _convertRawJsValue( + meta, + fs, + value, + fi.tagNumber, + fi.type, + registry, + ); + // In the case of an unknown enum value, the converted value may return + // null. The default enum value should be used in these cases, which is + // stored in the FieldInfo. + convertedValue ??= fi.defaultEnumValue; + repeated.add(convertedValue); + } +} + +void _appendRawJsMap( + BuilderInfo meta, + FieldSet fs, + JSArray jsonList, + MapFieldInfo fi, + ExtensionRegistry? registry, +) { + final entryMeta = fi.mapEntryBuilderInfo; + final map = fi.ensureMapField(meta, fs); + final length = jsonList.length; + + for (var i = 0; i < length; i++) { + final value = jsonList[i]; + final entryFieldSet = FieldSet(null, entryMeta); + + final convertedKey = _convertRawJsValue( + entryMeta, + entryFieldSet, + value.getProperty(mapKeyFieldNumber.toJS), + mapKeyFieldNumber, + fi.keyFieldType, + registry, + ); + var convertedValue = _convertRawJsValue( + entryMeta, + entryFieldSet, + value.getProperty(mapValueFieldNumber.toJS), + mapValueFieldNumber, + fi.valueFieldType, + registry, + ); + // In the case of an unknown enum value, the converted value may return + // null. The default enum value should be used in these cases, which is + // stored in the FieldInfo. + convertedValue ??= fi.defaultEnumValue; + map[convertedKey] = convertedValue; + } +} + +void _setRawJsField( + BuilderInfo meta, + FieldSet fs, + JSAny json, + FieldInfo fi, + ExtensionRegistry? registry, +) { + final value = _convertRawJsValue( + meta, + fs, + json, + fi.tagNumber, + fi.type, + registry, + ); + if (value == null) return; + // _convertRawJsValue throws exception when it fails to do conversion. + // Therefore we run _validateField for debug builds only to validate + // correctness of conversion. + assert(() { + fs.validateField(fi, value); + return true; + }()); + fs.setFieldUnchecked(meta, fi, value); +} + +/// Converts [value] from the JSON format to the Dart data type suitable for +/// inserting into the corresponding [GeneratedMessage] field. +/// +/// Returns the converted value. Returns `null` if it is an unknown enum value, +/// in which case the caller should figure out the default enum value to return +/// instead. +/// +/// Throws [ArgumentError] if it cannot convert the value. +Object? _convertRawJsValue( + BuilderInfo meta, + FieldSet fs, + JSAny value, + int tagNumber, + int fieldType, + ExtensionRegistry? registry, +) { + String expectedType; // for exception message + switch (PbFieldType.baseType(fieldType)) { + case PbFieldType.BOOL_BIT: + if (value.isA()) { + return value._as().toDart; + } else if (value.isA()) { + final dartStr = value._as().toDart; + if (dartStr == 'true') { + return true; + } else if (dartStr == 'false') { + return false; + } + } else if (value.isA()) { + final dartNum = value._as().toDartDouble; + if (dartNum == 1) { + return true; + } else if (dartNum == 0) { + return false; + } + } + expectedType = 'bool (true, false, "true", "false", 1, 0)'; + case PbFieldType.BYTES_BIT: + if (value.isA()) { + return base64Decode(value._as().toDart); + } + expectedType = 'Base64 String'; + case PbFieldType.STRING_BIT: + if (value.isA()) { + return value._as().toDart; + } + expectedType = 'String'; + case PbFieldType.FLOAT_BIT: + case PbFieldType.DOUBLE_BIT: + // Allow quoted values, although we don't emit them. + if (value.isA()) { + final jsNum = value._as(); + return _Number._isInteger(jsNum) ? jsNum.toDartInt : jsNum.toDartDouble; + } else if (value.isA()) { + return double.parse(value._as().toDart); + } + expectedType = 'num or stringified num'; + case PbFieldType.ENUM_BIT: + // Allow quoted values, although we don't emit them. + if (value.isA()) { + value = int.parse(value._as().toDart).toJS; + } + if (_Number._isInteger(value)) { + // The following call will return null if the enum value is unknown. + // In that case, we want the caller to ignore this value, so we return + // null from this method as well. + return meta.decodeEnum( + tagNumber, + registry, + value._as().toDartInt, + ); + } + expectedType = 'int or stringified int'; + case PbFieldType.INT32_BIT: + case PbFieldType.SINT32_BIT: + case PbFieldType.SFIXED32_BIT: + if (_Number._isInteger(value)) { + return value._as().toDartInt; + } + if (value.isA()) { + return int.parse(value._as().toDart); + } + expectedType = 'int or stringified int'; + case PbFieldType.UINT32_BIT: + case PbFieldType.FIXED32_BIT: + int? validatedValue; + if (_Number._isInteger(value)) { + validatedValue = value._as().toDartInt; + } + if (value.isA()) { + validatedValue = int.parse(value._as().toDart); + } + if (validatedValue != null && validatedValue < 0) { + validatedValue += 2 * (1 << 31); + } + if (validatedValue != null) return validatedValue; + expectedType = 'int or stringified int'; + case PbFieldType.INT64_BIT: + case PbFieldType.SINT64_BIT: + case PbFieldType.UINT64_BIT: + case PbFieldType.FIXED64_BIT: + case PbFieldType.SFIXED64_BIT: + if (_Number._isInteger(value)) { + return Int64(value._as().toDartInt); + } + if (value.isA()) { + return Int64.parseInt(value._as().toDart); + } + expectedType = 'int or stringified int'; + case PbFieldType.GROUP_BIT: + case PbFieldType.MESSAGE_BIT: + if (_getPrototypeOf(value).strictEquals(_objectPrototype).toDart) { + final subMessage = meta.makeEmptyMessage(tagNumber, registry); + _mergeFromRawJsMap( + subMessage.fieldSet, + value._as(), + registry, + ); + return subMessage; + } + expectedType = 'nested message or group'; + default: + throw ArgumentError( + 'Unknown type $fieldType when decoding a ' + '${meta.qualifiedMessageName} message field with tag $tagNumber.', + ); + } + throw ArgumentError( + 'Expected type $expectedType, got $value when decoding a ' + '${meta.qualifiedMessageName} message field with tag $tagNumber.', + ); +}