Firestore doesn't support JavaScript objects with custom prototypes?

Viewed 6143

I'm using the node Bigquery Package, to run a simple job. Looking at the results (say data) of the job the effective_date attribute look like this:

 effective_date: BigQueryDate { value: '2015-10-02' }

which is obviously an object within the returned data object.

Importing the returned json into Firestore gives the following error:

UnhandledPromiseRejectionWarning: Error: Argument "data" is not a 
valid Document. Couldn't serialize object of type "BigQueryDate". 
Firestore doesn't support JavaScript objects with custom prototypes 
(i.e. objects that were created via the 'new' operator).

Is there an elegant way to handle this? Does one need to iterate through the results and convert / remove all Objects?

3 Answers

The firestore Node.js client do not support serialization of custom classes.

You will find more explanation in this issue:
https://github.com/googleapis/nodejs-firestore/issues/143
"We explicitly decided to not support serialization of custom classes for the Web and Node.JS client"

A solution is to convert the nested object to a plain object. For example by using lodash or JSON.stringify.

firestore.collection('collectionName')
    .doc('id')
    .set(JSON.parse(JSON.stringify(myCustomObject)));

Here is a related post:
Firestore: Add Custom Object to db

Another way is less resource consuming:

firestore
  .collection('collectionName')
  .doc('id')
  .set(Object.assign({}, myCustomObject));

Note: it works only for objects without nested objects.

Also you may use class-transformer and it's classToPlain() along with exposeUnsetFields option to omit undefined values.

npm install class-transformer
or
yarn add class-transformer
import {classToPlain} from 'class-transformer';

firestore
  .collection('collectionName')
  .doc('id')
  .set(instanceToPlain(myCustomObject, {exposeUnsetFields: false}));

If you have a FirebaseFirestore.Timestamp object then don't use JSON.parse(JSON.stringify(obj)) or classToPlain(obj) as those will corrupt it while storing to Firestore.

It's better to use {...obj} method.

firestore
  .collection('collectionName')
  .doc('id')
  .set({...obj});

Note: do not use new operator for any nested objects inside document class, it'll not work. Instead, create an interface or type for nested object properties like this:

interface Profile {
    firstName: string;
    lastName: string;
}

class User {
    id = "";
    isPaid = false;
    profile: Profile = {
        firstName: "",
        lastName: "",
    };
}

const user = new User();

user.profile.firstName = "gorv";

await firestore.collection("users").add({...user});

And if you really wanna store class object consists of deeply nested more class objects then use this function to first convert it to plain object while preserving FirebaseFirestore.Timestamp methods.

const toPlainFirestoreObject = (o: any): any => {
  if (o && typeof o === "object" && !Array.isArray(o) && !isFirestoreTimestamp(o)) {
    return {
      ...Object.keys(o).reduce(
        (a: any, c: any) => ((a[c] = toPlainFirestoreObject(o[c])), a),
        {}
      ),
    };
  }
  return o;
};

function isFirestoreTimestamp(o: any): boolean {
  if (o && 
    Object.getPrototypeOf(o).toMillis &&
    Object.getPrototypeOf(o).constructor.name === "Timestamp"
  ) {
    return true;
  }
  return false;
}


const user = new User();

user.profile = new Profile();

user.profile.address = new Address();

await firestore.collection("users").add(toPlainFirestoreObject(user));
Related