Developer library · Dart, TypeScript, Java

firestore-proto-codec

Store protos in Firestore without inventing your own mapping

example.dart
// Write protos to documents
await doc.set(codec.encode(foo));

// Or individual fields
await doc.set({'foo': codec.encode(foo)});

// And read them back
final foo = codec.decode(snapshot.data()!, Foo());

Numbers stay numbers, timestamps stay timestamps, and blobs stay blobs.

In progress2026protobuf, firestore, dart, typescript, java

Protocol Buffers are a good way to define the shape of your data. Firestore is a good place to put that data. The join between them is where it gets awkward: there is no canonical way to write a protobuf message into a Firestore document, so every project invents one, and the inventions disagree.

firestore-proto-codec is that mapping, written down and pinned by tests. A pure function in each direction:

encode(Message)                                  -> Map<String, FirestoreValue>
decode(Map<String, FirestoreValue>, MessageType) -> Message

A message encodes to a map of Firestore-native values — the thing you hand to set(). Nested messages encode by the same rule, so writing a whole document and writing a single field are the same operation at different depths.

Why not proto3 JSON

Proto3 JSON is the obvious mapping, and it is wrong for Firestore in ways that fail silently — the write succeeds and the query is quietly incorrect.

proto3 JSON this codec
int64 String, so it sorts lexicographically and breaks every range query Integer
bytes base64 String Blob
Timestamp RFC-3339 String Timestamp, orderable and indexable
double NaN/±Inf String Double
Field names lowerCamelCase, and each runtime’s default differs the proto name, verbatim

Several of those choices are not conventions at all. Firestore’s own wire format is itself a protobuf: google.firestore.v1.Value declares bytes bytes_value, google.protobuf.Timestamp timestamp_value, and google.type.LatLng geo_point_value. For those types the mapping is the identity — the codec is recovering a correspondence that was already there.

Scope

Collection paths, document ids, security rules, index definitions, field masks, merge semantics, and schema migration are all deliberately out of scope. They are properties of an application’s schema rather than of the encoding. A codec that knows about collections is a persistence framework, and this is not one.

Where it stands

Draft, v0.1. The encoding lives in a written specification and is pinned by 24 shared conformance vectors; the Dart, TypeScript, and Java implementations all pass them, and a conforming implementation in any other language has to pass the same set. Not yet published, pending a decision on the extension number used by the per-field encoding options.