Search code examples
gogoogle-cloud-firestoreprotocol-buffersprotobuf-go

Unmarshal protojson formatted Firestore cloud event into map[string]interface{} or struct


Is there an easy way to unmarshal Firestore data in protojson format into a map[string]interface{} or struct without all the protobuf data type tags? i.e. flatten the protojson data.

I have a Google cloud function that's triggered whenever a new Firebase document is created (a "cloud event"). This cloud event includes context information including the document being modified in protojson format:

import (
    "google.golang.org/protobuf/encoding/protojson"
    "github.com/davecgh/go-spew/spew"
)

func CloudFunction(ctx context.Context, e event.Event) error {
    data := firestoredata.DocumentEventData{}
    _ = protojson.Unmarshal(e.DataEncoded, &data);

    spew.Dump(data)
}

--------Console Output--------

{
"oldValue": {},
"value": {
    "createTime": "2023-03-30T00:00:00.000000Z",
    "updateTime": "2023-03-30T00:00:00.000000Z",
    "name": "projects/myproject/databases/(default)/documents/collectionname/00000000-0000-0000-0000-000000000000",
    "fields": {
        "ID": {
        "stringValue": "00000000-0000-0000-0000-000000000000"
        },
        "action": {
            "stringValue": "serverDoSomething"
        },
        "payload": {
            "mapValue": {
                "fields": {
                    "questionsList": {
                        "arrayValue": {
                            "values": [
                                {
                                    "mapValue": {
                                        "fields": {
                                            "title": {
                                                "stringValue": "How do I fly a kite?"
                                            },
                                        }
                                    }
                                },
                                {
                                    "mapValue": {
                                        "fields": {
                                            "title": {
                                            "stringValue": "How do I fly a plane?"
                                            },
                                        }
                                    }
                                }
                            ]
                        }
                    }
                }
            }
        }
    }
},
"updateMask": {}
}

I want to marshal chunks of this protojson document into custom Go structs to easily validate each type of entry as shown below:

// CloudEventRequest is a struct that wraps around one or more data validation structs contained in the payload
CloudEventRequest {
    ID: "00000000-0000-0000-0000-000000000000"
    Action: "serverDoStuff"
    Payload: map{
        "questionsList": []Question{
            Question{
                Title: "How do I fly a kite?"
            },
            Question{
                Title: "How do I fly a plane?"
            }
        }
    }
}

The Firestore SDK includes a DataTo method to easily unmarshal the protojson formatted data into custom structs. I'm trying to do something very similar but already obtained the document data outside of the Firestore SDK.

// DataTo uses the document's fields to populate p, which can be a pointer to a map[string]interface{} or a pointer to a struct.

func (*firestore.DocumentSnapshot).DataTo(p interface{}) error
import (
    "context"
    "cloud.google.com/go/firestore"
)

func FirestoreRead(docEvent firestoredata.DocumentEventData) error {
    ctx := context.Background()

    ref := h.client.Collection("mycollection").Doc(docEvent.value.ID)
    docSnapshot, err := tx.Get(ref)

    dataValidationStruct := CloudEventRequest{}
    err = docSnapshot.DataTo(&dataValidationStruct)
}

Solution

  • I wrote an open source Go package named "firestruct" to solve this challenge. You can find it here: github.com/bennovw/firestruct Your feedback and contributions are most welcome!

    I ended up writing a function that recursively unwraps Firestore fields into a matching map. I then refactored the DataTo() method in cloud.google.com/go/firestore to unmarshal my map into a struct.