Firestore doesn't support JavaScript objects with custom prototypes?
Asked Answered
R

5

25

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?

Rompers answered 7/9, 2018 at 11:25 Comment(1)
Your probably need to add .doc() to the end of your firestore reference.Kommunarsk
P
26

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

Propagate answered 7/7, 2019 at 10:28 Comment(3)
Using it this way stops you to set field values such as increments and array operations!Digitigrade
Also it breaks Date types.Fancyfree
Also breaks reference types. strongly disagree with this answer. If you need a workaround, go the Object.assign({}) way.Lobeline
F
15

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}));
Fancyfree answered 2/1, 2020 at 20:15 Comment(2)
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 onFefeal
@AndreCytryn You can read more about date serialization issues here #53521174 and here #10286704Fancyfree
C
9

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));
Caplan answered 28/7, 2021 at 17:37 Comment(0)
D
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(),
});
Devote answered 17/4, 2023 at 0:30 Comment(0)
T
0

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()
  )));
Tardif answered 10/10, 2022 at 17:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.