google / google/protobuf.dart

map<string, int64> deserialization fails with "type 'int' is not a subtype of type 'Int64'" when value field is omitted

Open
#1,091 0 comments 1 reaction 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.