map<string, int64> deserialization fails with "type 'int' is not a subtype of type 'Int64'" when value field is omitted
- Dominant language
- Dart
- Stars
- 572
- Forks
- 196
- Avg merge
- 1h 59m
- Merged PRs (30d)
- 2
Description
# `map` deserialization fails when value is default (0)
## Description
When deserializing a protobuf message containing `map` fields where values are `0`, the Dart protobuf runtime crashes with a type error. This happens because the `makeDefault()` function returns `int` instead of `Int64` for all integer field types, including 64-bit ones.
The bug is triggered when the sender (e.g. Rust's `prost`) omits the value field in a map entry because it equals the default value (`0`), which is valid per proto3 semantics. The Dart deserializer then calls `makeDefault()` to fill in the missing value, but gets `int` instead of `Int64`.
## Steps to Reproduce
1. Define a proto message with `map`:
```protobuf
syntax = "proto3";
message Example {
map values = 1;
}
```
2. Serialize from Rust (prost) with a zero value:
```rust
let mut msg = Example::default();
msg.values.insert("key".into(), 0);
```
3. Deserialize in Dart:
```dart
final parsed = Example.fromBuffer(bytes);
// Throws: type 'int' is not a subtype of type 'Int64' of 'value'
```
## Error Message
The actual error is:
```
type 'int' is not a subtype of type 'Int64' of 'value'
```
When used through gRPC, this is swallowed and surfaces as:
```
gRPC Error (code: 15, codeName: DATA_LOSS, message: Error parsing response)
```
## Root Cause
In `protobuf-6.0.0/lib/src/protobuf/field_type.dart`, the `defaultForType()` method returns `INT_ZERO()` for all integer types:
```dart
static dynamic defaultForType(int type) {
switch (type & ~0x7) {
case PbFieldType._BOOL_BIT:
return DEFAULT_BOOL;
case PbFieldType._BYTES_BIT:
return EMPTY_BYTES;
case PbFieldType._STRING_BIT:
return '';
case PbFieldType._FLOAT_BIT:
case PbFieldType._DOUBLE_BIT:
return DOUBLE_ZERO;
case PbFieldType._ENUM_BIT:
return null;
case PbFieldType._INT32_BIT:
case PbFieldType._INT64_BIT: // ← should return Int64.ZERO
case PbFieldType._SINT32_BIT:
case PbFieldType._SINT64_BIT: // ← should return Int64.ZERO
case PbFieldType._UINT32_BIT:
case PbFieldType._UINT64_BIT: // ← should return Int64.ZERO
case PbFieldType._FIXED32_BIT:
case PbFieldType._FIXED64_BIT: // ← should return Int64.ZERO
case PbFieldType._SFIXED32_BIT:
case PbFieldType._SFIXED64_BIT: // ← should return Int64.ZERO
return INT_ZERO;
default:
return null;
}
}
static int INT_ZERO() => 0; // Returns Dart int, not Int64
```
This is consumed in `coded_buffer.dart` during map entry deserialization:
```dart
final key = entryFieldSet._values[0] ?? meta.byIndex[0].makeDefault!();
final value = entryFieldSet._values[1] ?? meta.byIndex[1].makeDefault!();
map[key] = value; // PbMap receives int → crash
```
When a map entry's value field is omitted (default value per proto3), `makeDefault()` is called, which delegates to `defaultForType()`, which returns `int` (0) instead of `Int64.ZERO`.
## Expected Behavior
`defaultForType()` should return `Int64.ZERO` for 64-bit integer types (`int64`, `sint64`, `uint64`, `fixed64`, `sfixed64`).
## Suggested Fix
```dart
static dynamic defaultForType(int type) {
switch (type & ~0x7) {
// ... other cases ...
case PbFieldType._INT32_BIT:
case PbFieldType._SINT32_BIT:
case PbFieldType._UINT32_BIT:
case PbFieldType._FIXED32_BIT:
case PbFieldType._SFIXED32_BIT:
return INT_ZERO;
case PbFieldType._INT64_BIT:
case PbFieldType._SINT64_BIT:
case PbFieldType._UINT64_BIT:
case PbFieldType._FIXED64_BIT:
case PbFieldType._SFIXED64_BIT:
return Int64.ZERO;
// ... other cases ...
}
}
```
## Environment
- `protobuf`: 6.0.0
- `protoc_plugin`: 25.0.0
- `grpc`: 5.1.0
- Dart SDK: 3.7.0
- Server: Rust with `prost` 0.13
- Proto syntax: proto3
## Notes
- The bug only affects map fields with int64 value types. Standalone `int64` fields (like `xp`, `credits`) work fine because they are handled differently during deserialization.
- The bug is only triggered when the value equals `0` (default), causing the sender to omit the value field per proto3 conventions.
- `grpcurl` (Go) parses the same server response correctly, confirming the server output is valid.
Contributor guide
Research direction
Start in protobuf-6.0.0/lib/src/protobuf/field_type.dart at defaultForType(), then follow its use from coded_buffer.dart during map-entry deserialization. Reproduce the omitted-value case for a map and add regression coverage; done means zero-valued omitted entries deserialize without the int/Int64 type error.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- dart
- Domain
- api
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 58/100