27

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?

1
  • Your probably need to add .doc() to the end of your firestore reference. Commented Oct 29, 2018 at 22:19

5 Answers 5

25

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

Sign up to request clarification or add additional context in comments.

4 Comments

Using it this way stops you to set field values such as increments and array operations!
Also it breaks Date types.
Also breaks reference types. strongly disagree with this answer. If you need a workaround, go the Object.assign({}) way.
{...myCustomObject} is a better answer.
17

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}));

2 Comments

this works better for me because if you have a date in your custom object, firestore will recornize it as timestamp. If you do the JSON.stringify() your date will become a string and this could be a nightmare later on
@AndreCytryn You can read more about date serialization issues here stackoverflow.com/questions/53520674/… and here stackoverflow.com/questions/10286204/…
10

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));

Comments

2

I ran into this with converting a module to a class in Firestore. The issue was that I was using previously an admin firestore instance and referencing some field info from @google-cloud instead of using the methods in the firebase admin instance

const admin = require('firebase-admin');
const { FieldValue } = require('@google-cloud/firestore');

await accountDocRef.set({
  createdAt: FieldValue.serverTimestamp(),
});

should use the references in the admin package instead:

const admin = require('firebase-admin');

await accountDocRef.set({
  createdAt: admin.firestore.FieldValue.serverTimestamp(),
});

Comments

1

Serializes a value to a valid Firestore Document data, including object and its childs and Array and its items

export function serializeFS(value) {
    const isDate = (value) => {
        if(value instanceof Date || value instanceof firestore.Timestamp){
            return true;
        }
        try {
            if(value.toDate() instanceof Date){
                return true;
            }
        } catch (e){}

        return false;
    };

    if(value == null){
        return null;
    }
    if(
        typeof value == "boolean" ||
        typeof value == "bigint" ||
        typeof value == "string" ||
        typeof value == "symbol" ||
        typeof value == "number" ||
        isDate(value) ||
        value instanceof firestore.FieldValue
    ) {
        return value;
    }

    if(Array.isArray(value)){
        return (value as Array<any>).map((v) => serializeFS(v));
    }

    const res = {};
    for(const key of Object.keys(value)){
        res[key] = serializeFS(value[key]);
    }
    return res;
}

Usage:

await db().collection('products').doc()
  .set(serializeFS(
     new ProductEntity('something', 123, FieldValue.serverTimestamp()
  )));

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.